mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-26 13:57:36 +02:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -6,7 +6,7 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: check-duplicate
|
id: check-duplicate
|
||||||
attributes:
|
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:
|
options:
|
||||||
- label: 'Yes'
|
- label: 'Yes'
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ permissions: write-all
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Docker image tag (e.g. 1.12.0 or latest)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- 'v*.*.*'
|
||||||
@@ -33,11 +38,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=raw,value=${{ inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }}
|
||||||
type=ref,event=pr
|
type=semver,pattern={{version}},enable=${{ github.event_name == 'push' }}
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{major}}.{{minor}},enable=${{ github.event_name == 'push' }}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}},enable=${{ github.event_name == 'push' }}
|
||||||
type=semver,pattern={{major}}
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Setup Docker buildx
|
- name: Setup Docker buildx
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,8 @@ WORKDIR /app
|
|||||||
# Copy package.json first to cache node_modules
|
# Copy package.json first to cache node_modules
|
||||||
COPY package.json pnpm-lock.yaml .
|
COPY package.json pnpm-lock.yaml .
|
||||||
|
|
||||||
RUN npm install -g pnpm
|
# Match CI (pnpm/action-setup version: 10). Latest pnpm 11 fails install without approve-builds.
|
||||||
|
RUN corepack enable && corepack prepare pnpm@10 --activate
|
||||||
|
|
||||||
RUN pnpm install
|
RUN pnpm install
|
||||||
|
|
||||||
|
|||||||
@@ -114,8 +114,11 @@ These variables override app settings **on first run** when no persisted setting
|
|||||||
|
|
||||||
| Setting path | Default | Env variable | Available values / Description |
|
| 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.enabled` | `false` | `FS_AUTO_DJ_ENABLED` | `true` / `false`. |
|
||||||
| `autoDJ.itemCount` | `5` | `FS_AUTO_DJ_ITEM_COUNT` | Number of items to add. |
|
| `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). |
|
| `autoDJ.timing` | `1` | `FS_AUTO_DJ_TIMING` | Timing value (number). |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "1.11.0",
|
"version": "1.12.1",
|
||||||
"description": "A modern self-hosted music player.",
|
"description": "A modern self-hosted music player.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"subsonic",
|
"subsonic",
|
||||||
@@ -189,6 +189,7 @@
|
|||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"electron",
|
"electron",
|
||||||
|
"electron-winstaller",
|
||||||
"esbuild"
|
"esbuild"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -88,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_TRANSLATION_TARGET_LANGUAGE = "${FS_LYRICS_TRANSLATION_TARGET_LANGUAGE}";
|
||||||
window.FS_LYRICS_ALIGNMENT = "${FS_LYRICS_ALIGNMENT}";
|
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_ENABLED = "${FS_AUTO_DJ_ENABLED}";
|
||||||
window.FS_AUTO_DJ_ITEM_COUNT = "${FS_AUTO_DJ_ITEM_COUNT}";
|
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_AUTO_DJ_TIMING = "${FS_AUTO_DJ_TIMING}";
|
||||||
|
|
||||||
window.FS_CSS_CONTENT = "${FS_CSS_CONTENT}";
|
window.FS_CSS_CONTENT = "${FS_CSS_CONTENT}";
|
||||||
|
|||||||
+349
-15
@@ -2,32 +2,48 @@
|
|||||||
"action": {
|
"action": {
|
||||||
"addToFavorites": "إضافة الى $t(entity.favorite, {\"count\": 2})",
|
"addToFavorites": "إضافة الى $t(entity.favorite, {\"count\": 2})",
|
||||||
"addToPlaylist": "إضافة الى $t(entity.playlist, {\"count\": 1})",
|
"addToPlaylist": "إضافة الى $t(entity.playlist, {\"count\": 1})",
|
||||||
"clearQueue": "مسح قائمة الإنتظار",
|
"clearQueue": "مسح قائمة التشغيل",
|
||||||
"createPlaylist": "إنشاء $t(entity.playlist, {\"count\": 1})",
|
"createPlaylist": "إنشاء $t(entity.playlist, {\"count\": 1})",
|
||||||
"deletePlaylist": "حذف $t(entity.playlist, {\"count\": 1})",
|
"deletePlaylist": "حذف $t(entity.playlist, {\"count\": 1})",
|
||||||
"deselectAll": "إلغاء تحديد الكل",
|
"deselectAll": "إلغاء تحديد الكل",
|
||||||
"editPlaylist": "تعديل $t(entity.playlist, {\"count\": 1})",
|
"editPlaylist": "تعديل $t(entity.playlist, {\"count\": 1})",
|
||||||
"goToPage": "اذهب الى صفحة",
|
"goToPage": "اذهب الى الصفحة",
|
||||||
"moveToNext": "الذهاب الى التالي",
|
"moveToNext": "نقل إلى التالي",
|
||||||
"moveToBottom": "الذهاب الى الأسفل",
|
"moveToBottom": "نقل إلى الأسفل",
|
||||||
"moveToTop": "الذهاب الى الأعلى",
|
"moveToTop": "نقل إلى الأعلى",
|
||||||
"refresh": "$t(common.refresh)",
|
"refresh": "$t(common.refresh)",
|
||||||
"removeFromFavorites": "حذف من $t(entity.favorite, {\"count\": 2})",
|
"removeFromFavorites": "حذف من $t(entity.favorite, {\"count\": 2})",
|
||||||
"removeFromPlaylist": "حذف من $t(entity.playlist, {\"count\": 1})",
|
"removeFromPlaylist": "حذف من $t(entity.playlist, {\"count\": 1})",
|
||||||
"removeFromQueue": "حذف من قائمة الإنتظار",
|
"removeFromQueue": "حذف من قائمة التشغيل",
|
||||||
"setRating": "تحديد التقييم",
|
"setRating": "تحديد التقييم",
|
||||||
"toggleSmartPlaylistEditor": "تشغيل / إطفاء وضع التعديل لـ $t(entity.smartPlaylist)",
|
"toggleSmartPlaylistEditor": "إظهار / إخفاء وضع التعديل لـ $t(entity.smartPlaylist)",
|
||||||
"viewPlaylists": "إظهار $t(entity.playlist, {\"count\": 2})",
|
"viewPlaylists": "عرض $t(entity.playlist, {\"count\": 2})",
|
||||||
"openIn": {
|
"openIn": {
|
||||||
"lastfm": "فتح في Last.fm",
|
"lastfm": "فتح في Last.fm",
|
||||||
"musicbrainz": "فتح في MusicBrainz"
|
"musicbrainz": "فتح في MusicBrainz",
|
||||||
|
"listenbrainz": "فتح في ListenBrainz",
|
||||||
|
"qobuz": "فتح في Qobuz",
|
||||||
|
"spotify": "فتح في Spotify"
|
||||||
},
|
},
|
||||||
"addOrRemoveFromSelection": "إضافة أو إزالة من الإختيارات",
|
"addOrRemoveFromSelection": "إضافة أو إزالة من الإختيارات",
|
||||||
"selectRangeOfItems": "اختر مجموعة من العناصر",
|
"selectRangeOfItems": "اختر مجموعة من العناصر",
|
||||||
"goToCurrent": "الانتقال إلى العنصر الحالي",
|
"goToCurrent": "الانتقال إلى العنصر الحالي",
|
||||||
"createRadioStation": "يخلق $t(entity.radioStation, {\"count\": 1})",
|
"createRadioStation": "إنشاء $t(entity.radioStation, {\"count\": 1})",
|
||||||
"deleteRadioStation": "يمسح $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": {
|
"common": {
|
||||||
"action_zero": "عملية",
|
"action_zero": "عملية",
|
||||||
@@ -39,13 +55,13 @@
|
|||||||
"add": "إضافة",
|
"add": "إضافة",
|
||||||
"additionalParticipants": "مشاركين إضافيين",
|
"additionalParticipants": "مشاركين إضافيين",
|
||||||
"newVersion": "تم تثبيت تحديث جديد {{version}}",
|
"newVersion": "تم تثبيت تحديث جديد {{version}}",
|
||||||
"viewReleaseNotes": "عرض معلومات الإصدار",
|
"viewReleaseNotes": "عرض ملاحظات الإصدار",
|
||||||
"albumGain": "مستوى صوت الألبوم",
|
"albumGain": "مستوى صوت الألبوم",
|
||||||
"albumPeak": "اعلى مستوى للألبوم",
|
"albumPeak": "اعلى مستوى للألبوم",
|
||||||
"areYouSure": "هل أنت متأكد؟",
|
"areYouSure": "هل أنت متأكد؟",
|
||||||
"ascending": "تصاعدي",
|
"ascending": "تصاعدي",
|
||||||
"backward": "خلف",
|
"backward": "خلف",
|
||||||
"biography": "سيرة",
|
"biography": "السيرة",
|
||||||
"bitDepth": "عمق البت",
|
"bitDepth": "عمق البت",
|
||||||
"bitrate": "معدل البت (البت ريت)",
|
"bitrate": "معدل البت (البت ريت)",
|
||||||
"bpm": "نبضة في الدقيقة",
|
"bpm": "نبضة في الدقيقة",
|
||||||
@@ -141,7 +157,35 @@
|
|||||||
"unknown": "غير معروف",
|
"unknown": "غير معروف",
|
||||||
"version": "الإصدار",
|
"version": "الإصدار",
|
||||||
"year": "السنة",
|
"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": {
|
"entity": {
|
||||||
"album_zero": "الالبوم",
|
"album_zero": "الالبوم",
|
||||||
@@ -155,6 +199,296 @@
|
|||||||
"albumArtist_two": "فنان الالبومين",
|
"albumArtist_two": "فنان الالبومين",
|
||||||
"albumArtist_few": "فنان الالبومات",
|
"albumArtist_few": "فنان الالبومات",
|
||||||
"albumArtist_many": "فنان الالبومات",
|
"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": "الأغاني المفضلة"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -827,7 +827,6 @@
|
|||||||
"notify_description": "Mostra notificacions quan la cançó actual canviï",
|
"notify_description": "Mostra notificacions quan la cançó actual canviï",
|
||||||
"transcode": "Activa la transcodificació",
|
"transcode": "Activa la transcodificació",
|
||||||
"autoDJ": "DJ automàtic",
|
"autoDJ": "DJ automàtic",
|
||||||
"autoDJ_description": "Afegeix cançons similars a la cua automàticament",
|
|
||||||
"autoDJ_itemCount": "Número d'elements",
|
"autoDJ_itemCount": "Número d'elements",
|
||||||
"autoDJ_itemCount_description": "El nombre d'elements que s'intenten afegir a la cua quan el DJ automàtic està activat",
|
"autoDJ_itemCount_description": "El nombre d'elements que s'intenten afegir a la cua quan el DJ automàtic està activat",
|
||||||
"autoDJ_timing": "Temps",
|
"autoDJ_timing": "Temps",
|
||||||
|
|||||||
@@ -234,7 +234,7 @@
|
|||||||
"customCssEnable": "Povolit vlastní CSS",
|
"customCssEnable": "Povolit vlastní CSS",
|
||||||
"customCssEnable_description": "Umožnit psaní vlastního CSS",
|
"customCssEnable_description": "Umožnit psaní vlastního CSS",
|
||||||
"customCssNotice": "Varování: i když provádíme určitou sanitizaci (zakázáním URL() a content:), může používání CSS stále představovat riziko změnami rozhraní",
|
"customCssNotice": "Varování: i když provádíme určitou sanitizaci (zakázáním URL() a content:), může používání CSS stále představovat riziko změnami rozhraní",
|
||||||
"customCss_description": "Vlastní CSS obsah. Upozornění: vlastnosti content a vzdálené URL jsou zakázané. Níže je zobrazen náhled vašeho obsahu. Další pole, která jste nenastavili, jsou přítomna z důvodu sanitizace",
|
"customCss_description": "Vlastní CSS obsah. Upozornění: vlastnosti content a vzdálené URL jsou zakázané. Níže je zobrazen náhled vašeho obsahu. Další pole, která jste nenastavili, jsou přítomna z důvodu sanitizace. Počítačový Feishin čte a zapisuje soubor custom.css do konfiguračního adresáře aplikace a znovu jej načte po jeho změně",
|
||||||
"customCss": "Vlastní css",
|
"customCss": "Vlastní css",
|
||||||
"webAudio": "Použít webový zvuk",
|
"webAudio": "Použít webový zvuk",
|
||||||
"webAudio_description": "Použít webový zvuk. Tím povolíte pokročilé funkce jako ReplayGain. Zakažte, pokud se objeví problémy",
|
"webAudio_description": "Použít webový zvuk. Tím povolíte pokročilé funkce jako ReplayGain. Zakažte, pokud se objeví problémy",
|
||||||
@@ -344,9 +344,8 @@
|
|||||||
"playerFilters_description": "Vynechat skladby z přidání do fronty na základě následujících kritérií",
|
"playerFilters_description": "Vynechat skladby z přidání do fronty na základě následujících kritérií",
|
||||||
"playerbarSlider_description": "Vlnová křivka není doporučena, pokud se nacházíte na pomalém nebo měřeném internetovém připojení",
|
"playerbarSlider_description": "Vlnová křivka není doporučena, pokud se nacházíte na pomalém nebo měřeném internetovém připojení",
|
||||||
"autoDJ": "Automatický DJ",
|
"autoDJ": "Automatický DJ",
|
||||||
"autoDJ_description": "Automaticky přidávat podobné skladby do fronty",
|
|
||||||
"autoDJ_itemCount": "Počet položek",
|
"autoDJ_itemCount": "Počet položek",
|
||||||
"autoDJ_itemCount_description": "Počet položek, které se pokusíme přidat do fronty po povolení automatického DJ",
|
"autoDJ_itemCount_description": "Počet položek, které se pokusíme přidat do fronty",
|
||||||
"autoDJ_timing": "Časování",
|
"autoDJ_timing": "Časování",
|
||||||
"autoDJ_timing_description": "Počet skladeb zbývajících ve frontě před spuštěním automatického DJ",
|
"autoDJ_timing_description": "Počet skladeb zbývajících ve frontě před spuštěním automatického DJ",
|
||||||
"logLevel": "Úroveň protokolu",
|
"logLevel": "Úroveň protokolu",
|
||||||
@@ -448,7 +447,16 @@
|
|||||||
"sidebarPlaylistMode_description": "Jak je každý seznam skladeb zobrazen v seznamu v postranní liště",
|
"sidebarPlaylistMode_description": "Jak je každý seznam skladeb zobrazen v seznamu v postranní liště",
|
||||||
"sidebarPlaylistMode": "Režim seznamů skladeb v postranní liště",
|
"sidebarPlaylistMode": "Režim seznamů skladeb v postranní liště",
|
||||||
"sidebarPlaylistMode_optionCompact": "Kompaktní",
|
"sidebarPlaylistMode_optionCompact": "Kompaktní",
|
||||||
"sidebarPlaylistMode_optionExpanded": "Rozšířený"
|
"sidebarPlaylistMode_optionExpanded": "Rozšířený",
|
||||||
|
"autoDJ_mode": "Režim",
|
||||||
|
"autoDJ_mode_albums": "Alba",
|
||||||
|
"autoDJ_mode_description": "Vyberte, zda do fronty přidávat skladby nebo celá alba",
|
||||||
|
"autoDJ_mode_songs": "Skladby",
|
||||||
|
"autoDJ_enabled": "Povolit automatického DJ",
|
||||||
|
"autoDJ_albumStrategy": "Režim výběru alb",
|
||||||
|
"autoDJ_songStrategy": "Režim výběru skladeb",
|
||||||
|
"autoDJ_strategy_option_library_random": "Náhodně",
|
||||||
|
"autoDJ_strategy_option_similar": "Podobné"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"editPlaylist": "Upravit $t(entity.playlist, {\"count\": 1})",
|
"editPlaylist": "Upravit $t(entity.playlist, {\"count\": 1})",
|
||||||
@@ -624,7 +632,8 @@
|
|||||||
"newVersionAvailable": "Je dostupná nová verze",
|
"newVersionAvailable": "Je dostupná nová verze",
|
||||||
"numberOfResults": "{{numberOfResults}} výsledků",
|
"numberOfResults": "{{numberOfResults}} výsledků",
|
||||||
"grouping": "Seskupování",
|
"grouping": "Seskupování",
|
||||||
"back": "Zpět"
|
"back": "Zpět",
|
||||||
|
"openFolder": "Otevřít složku"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -1123,7 +1132,12 @@
|
|||||||
"input_played": "Přehrát filtr",
|
"input_played": "Přehrát filtr",
|
||||||
"input_played_optionAll": "Všechny skladby",
|
"input_played_optionAll": "Všechny skladby",
|
||||||
"input_played_optionUnplayed": "Pouze nepřehrané skladby",
|
"input_played_optionUnplayed": "Pouze nepřehrané skladby",
|
||||||
"input_played_optionPlayed": "Pouze přehrané skladby"
|
"input_played_optionPlayed": "Pouze přehrané skladby",
|
||||||
|
"input_kind_albums": "Alba",
|
||||||
|
"input_kind_songs": "Skladby",
|
||||||
|
"input_kind": "Náhodný výběr",
|
||||||
|
"input_limit_albums": "Kolik alb?",
|
||||||
|
"input_limit_songs": "Kolik skladeb?"
|
||||||
},
|
},
|
||||||
"saveQueue": {
|
"saveQueue": {
|
||||||
"success": "Fronta přehrávání uložena na server"
|
"success": "Fronta přehrávání uložena na server"
|
||||||
|
|||||||
@@ -695,7 +695,6 @@
|
|||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"autoDJ": "Auto-DJ",
|
"autoDJ": "Auto-DJ",
|
||||||
"autoDJ_description": "Tilføj automatisk lignende sange til køen",
|
|
||||||
"autoDJ_itemCount": "Antal elementer",
|
"autoDJ_itemCount": "Antal elementer",
|
||||||
"autoDJ_itemCount_description": "Antallet af elementer der forsøges tilføjet til køen, når auto-DJ er aktiveret",
|
"autoDJ_itemCount_description": "Antallet af elementer der forsøges tilføjet til køen, når auto-DJ er aktiveret",
|
||||||
"autoDJ_timing": "Tidspunkt",
|
"autoDJ_timing": "Tidspunkt",
|
||||||
|
|||||||
+62
-51
@@ -13,8 +13,8 @@
|
|||||||
"removeFromPlaylist": "Aus $t(entity.playlist, {\"count\": 1}) entfernen",
|
"removeFromPlaylist": "Aus $t(entity.playlist, {\"count\": 1}) entfernen",
|
||||||
"viewPlaylists": "$t(entity.playlist, {\"count\": 2}) anzeigen",
|
"viewPlaylists": "$t(entity.playlist, {\"count\": 2}) anzeigen",
|
||||||
"refresh": "$t(common.refresh)",
|
"refresh": "$t(common.refresh)",
|
||||||
"removeFromQueue": "Aus wiedergabeliste entfernen",
|
"removeFromQueue": "Aus Wiedergabeliste entfernen",
|
||||||
"setRating": "Bewerten",
|
"setRating": "Bewertung setzen",
|
||||||
"toggleSmartPlaylistEditor": "Editor für $t(entity.smartPlaylist) ein-/ausblenden",
|
"toggleSmartPlaylistEditor": "Editor für $t(entity.smartPlaylist) ein-/ausblenden",
|
||||||
"removeFromFavorites": "Aus $t(entity.favorite, {\"count\": 2}) entfernen",
|
"removeFromFavorites": "Aus $t(entity.favorite, {\"count\": 2}) entfernen",
|
||||||
"openIn": {
|
"openIn": {
|
||||||
@@ -41,12 +41,14 @@
|
|||||||
"selectRangeOfItems": "Wählen sie eine reihe von elementen",
|
"selectRangeOfItems": "Wählen sie eine reihe von elementen",
|
||||||
"holdToMoveToTop": "Halten um nach oben zu bewegen",
|
"holdToMoveToTop": "Halten um nach oben zu bewegen",
|
||||||
"holdToMoveToBottom": "Halten um nach unten zu bewegen",
|
"holdToMoveToBottom": "Halten um nach unten zu bewegen",
|
||||||
"goToCurrent": "Zu aktuellem eintrag wechseln"
|
"goToCurrent": "Zu aktuellem Eintrag wechseln",
|
||||||
|
"collapseAllFolders": "Alle Ordner einklappen",
|
||||||
|
"expandAllFolders": "Alle Ordner ausklappen"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"backward": "Zurück",
|
"backward": "Zurück",
|
||||||
"increase": "Erhöhen",
|
"increase": "Erhöhen",
|
||||||
"rating": "Wertung",
|
"rating": "Bewertung",
|
||||||
"bpm": "Bpm",
|
"bpm": "Bpm",
|
||||||
"refresh": "Aktualisieren",
|
"refresh": "Aktualisieren",
|
||||||
"unknown": "Unbekannt",
|
"unknown": "Unbekannt",
|
||||||
@@ -165,9 +167,10 @@
|
|||||||
"rename": "Umbenennen",
|
"rename": "Umbenennen",
|
||||||
"filter_single": "Einzeln",
|
"filter_single": "Einzeln",
|
||||||
"filter_multiple": "Mehrfach",
|
"filter_multiple": "Mehrfach",
|
||||||
"retry": "Wiederholen",
|
"retry": "Erneut versuchen",
|
||||||
"newVersionAvailable": "Eine neue version ist verfügbar",
|
"newVersionAvailable": "Eine neue version ist verfügbar",
|
||||||
"numberOfResults": "{{numberOfResults}} ergebnisse"
|
"numberOfResults": "{{numberOfResults}} ergebnisse",
|
||||||
|
"openFolder": "Verzeichnis öffnen"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden",
|
"remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden",
|
||||||
@@ -177,7 +180,7 @@
|
|||||||
"remotePortError": "Beim Versuch, den Remote-Server-Port festzulegen, ist ein Fehler aufgetreten",
|
"remotePortError": "Beim Versuch, den Remote-Server-Port festzulegen, ist ein Fehler aufgetreten",
|
||||||
"serverRequired": "Server benötigt",
|
"serverRequired": "Server benötigt",
|
||||||
"authenticationFailed": "Authentifizierung fehlgeschlagen",
|
"authenticationFailed": "Authentifizierung fehlgeschlagen",
|
||||||
"apiRouteError": "Anforderung kann nicht weitergeleitet werden",
|
"apiRouteError": "Anfrage kann nicht weitergeleitet werden",
|
||||||
"genericError": "Ein Fehler ist aufgetreten",
|
"genericError": "Ein Fehler ist aufgetreten",
|
||||||
"credentialsRequired": "Anmeldeinformationen erforderlich",
|
"credentialsRequired": "Anmeldeinformationen erforderlich",
|
||||||
"sessionExpiredError": "Deine Sitzung ist abgelaufen",
|
"sessionExpiredError": "Deine Sitzung ist abgelaufen",
|
||||||
@@ -189,13 +192,13 @@
|
|||||||
"audioDeviceFetchError": "Beim Versuch, Audiogeräte abzurufen, ist ein Fehler aufgetreten",
|
"audioDeviceFetchError": "Beim Versuch, Audiogeräte abzurufen, ist ein Fehler aufgetreten",
|
||||||
"invalidServer": "Ungültiger Server",
|
"invalidServer": "Ungültiger Server",
|
||||||
"loginRateError": "Zu viele Anmeldeversuche, bitte versuche es in einigen Sekunden erneut",
|
"loginRateError": "Zu viele Anmeldeversuche, bitte versuche es in einigen Sekunden erneut",
|
||||||
"badAlbum": "Sie sehen diese Seite, weil dieses Lied nicht Teil eines Albums ist. Dieses Problem tritt meist auf, wenn sich ein Lied im Überordner befindet. Jellyfin gruppiert Tracks nur, wenn diese sich innerhalb eines Ordners befinden",
|
"badAlbum": "Sie sehen diese Seite, weil dieses Lied nicht Teil eines Albums ist. Dieses Problem tritt meist auf, wenn sich ein Lied im Überordner befindet. Jellyfin gruppiert Tracks nur, wenn diese sich innerhalb eines Verzeichnisses befinden",
|
||||||
"networkError": "Ein Netzwerkfehler ist aufgetreten",
|
"networkError": "Ein Netzwerkfehler ist aufgetreten",
|
||||||
"openError": "Datei kann nicht geöffnet werden",
|
"openError": "Datei kann nicht geöffnet werden",
|
||||||
"badValue": "Ungültige option \"{{value}}\". Dieser Wert existiert nicht mehr",
|
"badValue": "Ungültige option \"{{value}}\". Dieser Wert existiert nicht mehr",
|
||||||
"notificationDenied": "Berechtigungen über Benachrichtigungen wurden verweigert. Diese Einstellung hat keinen Effekt",
|
"notificationDenied": "Berechtigungen über Benachrichtigungen wurden verweigert. Diese Einstellung hat keinen Effekt",
|
||||||
"saveQueueFailed": "Wiedergabeliste konnte nicht gespeichert werden",
|
"saveQueueFailed": "Wiedergabeliste konnte nicht gespeichert werden",
|
||||||
"multipleServerSaveQueueError": "Die Wiedergabeliste enthält einen oder mehrere Titel, die nicht vom aktuellen Server stammen. dies wird nicht unterstützt",
|
"multipleServerSaveQueueError": "Die Wiedergabeliste enthält einen oder mehrere Titel, die nicht vom aktuellen Server stammen. Dies wird nicht unterstützt",
|
||||||
"noNetwork": "Server nicht verfügbar",
|
"noNetwork": "Server nicht verfügbar",
|
||||||
"noNetworkDescription": "Verbindung zum Server konnte nicht hergestellt werden",
|
"noNetworkDescription": "Verbindung zum Server konnte nicht hergestellt werden",
|
||||||
"invalidJson": "JSON ungültig",
|
"invalidJson": "JSON ungültig",
|
||||||
@@ -218,7 +221,7 @@
|
|||||||
"recentlyAdded": "Kürzlich hinzugefügt",
|
"recentlyAdded": "Kürzlich hinzugefügt",
|
||||||
"note": "Hinweis",
|
"note": "Hinweis",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"dateAdded": "Datum hinzugefügt",
|
"dateAdded": "Hinzugefügt am",
|
||||||
"releaseDate": "Veröffentlichungsdatum",
|
"releaseDate": "Veröffentlichungsdatum",
|
||||||
"albumCount": "$t(entity.album, {\"count\": 2}) anzahl",
|
"albumCount": "$t(entity.album, {\"count\": 2}) anzahl",
|
||||||
"communityRating": "Community-wertung",
|
"communityRating": "Community-wertung",
|
||||||
@@ -248,7 +251,8 @@
|
|||||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||||
"explicitStatus": "$t(common.explicitStatus)",
|
"explicitStatus": "$t(common.explicitStatus)",
|
||||||
"matchAnd": "Und",
|
"matchAnd": "Und",
|
||||||
"matchOr": "Oder"
|
"matchOr": "Oder",
|
||||||
|
"sortName": "Sortierungsname"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"deletePlaylist": {
|
"deletePlaylist": {
|
||||||
@@ -306,7 +310,7 @@
|
|||||||
"editPlaylist": {
|
"editPlaylist": {
|
||||||
"title": "Bearbeite $t(entity.playlist, {\"count\": 1})",
|
"title": "Bearbeite $t(entity.playlist, {\"count\": 1})",
|
||||||
"success": "$t(entity.playlist, {\"count\": 1}) erfolgreich aktualisiert",
|
"success": "$t(entity.playlist, {\"count\": 1}) erfolgreich aktualisiert",
|
||||||
"publicJellyfinNote": "Jellyfin legt aus irgendwelchen Gründen nicht offen ob eine Wiedergabeliste öffentlich ist oder nicht. Wenn du möchtest, dass sie öffentlich bleibt, wähle bitte diese Option aus"
|
"publicJellyfinNote": "Jellyfin legt aus irgendwelchen Gründen nicht offen, ob eine Wiedergabeliste öffentlich ist oder nicht. Wenn du möchtest, dass sie öffentlich bleibt, wähle bitte diese Option aus"
|
||||||
},
|
},
|
||||||
"lyricSearch": {
|
"lyricSearch": {
|
||||||
"title": "Songtext suche",
|
"title": "Songtext suche",
|
||||||
@@ -324,12 +328,12 @@
|
|||||||
"successMustClick": "Freigabe erfolgreich erstellt. Hier klicken um diese zu öffnen"
|
"successMustClick": "Freigabe erfolgreich erstellt. Hier klicken um diese zu öffnen"
|
||||||
},
|
},
|
||||||
"privateMode": {
|
"privateMode": {
|
||||||
"enabled": "Privatmodus aktiviert, Wiedergabe-Status wird externen Quellen nicht preisgegeben",
|
"enabled": "Privater Modus aktiviert, Wiedergabe-Status wird externen Quellen nicht preisgegeben",
|
||||||
"disabled": "Privatmodus deaktiviert, Wiedergabe-Status wird externen Quellen preisgegeben",
|
"disabled": "Privater Modus deaktiviert, Wiedergabe-Status wird externen Quellen preisgegeben",
|
||||||
"title": "Privatmodus"
|
"title": "Privater Modus"
|
||||||
},
|
},
|
||||||
"largeFetchConfirmation": {
|
"largeFetchConfirmation": {
|
||||||
"title": "Elemente der wiedergabeliste hinzufügen",
|
"title": "Elemente der Wiedergabeliste hinzufügen",
|
||||||
"description": "Diese Aktion fügt alle Elemente in der aktuell gefilterten Ansicht hinzu"
|
"description": "Diese Aktion fügt alle Elemente in der aktuell gefilterten Ansicht hinzu"
|
||||||
},
|
},
|
||||||
"shuffleAll": {
|
"shuffleAll": {
|
||||||
@@ -344,7 +348,7 @@
|
|||||||
"input_played": "Wiedergabefilter"
|
"input_played": "Wiedergabefilter"
|
||||||
},
|
},
|
||||||
"saveQueue": {
|
"saveQueue": {
|
||||||
"success": "Wiedergabeliste auf server gespeichert"
|
"success": "Wiedergabeliste auf Server gespeichert"
|
||||||
},
|
},
|
||||||
"createRadioStation": {
|
"createRadioStation": {
|
||||||
"success": "Radiosender erfolgreich erstellt",
|
"success": "Radiosender erfolgreich erstellt",
|
||||||
@@ -355,7 +359,7 @@
|
|||||||
},
|
},
|
||||||
"lyricsExport": {
|
"lyricsExport": {
|
||||||
"input_offset": "$t(setting.lyricOffset)",
|
"input_offset": "$t(setting.lyricOffset)",
|
||||||
"export": "Songtexte exportieren",
|
"export": "Liedtext exportieren",
|
||||||
"input_synced": "Synchronisierte songtexte exportieren"
|
"input_synced": "Synchronisierte songtexte exportieren"
|
||||||
},
|
},
|
||||||
"editRadioStation": {
|
"editRadioStation": {
|
||||||
@@ -365,14 +369,14 @@
|
|||||||
"entity": {
|
"entity": {
|
||||||
"genre_one": "Genre",
|
"genre_one": "Genre",
|
||||||
"genre_other": "Genres",
|
"genre_other": "Genres",
|
||||||
"playlistWithCount_one": "{{count}} wiedergabeliste",
|
"playlistWithCount_one": "{{count}} Wiedergabeliste",
|
||||||
"playlistWithCount_other": "{{count}} wiedergabelisten",
|
"playlistWithCount_other": "{{count}} Wiedergabelisten",
|
||||||
"playlist_one": "Wiedergabeliste",
|
"playlist_one": "Wiedergabeliste",
|
||||||
"playlist_other": "Wiedergabelisten",
|
"playlist_other": "Wiedergabelisten",
|
||||||
"artist_one": "Interpret",
|
"artist_one": "Interpret",
|
||||||
"artist_other": "Interpreten",
|
"artist_other": "Interpreten",
|
||||||
"folderWithCount_one": "{{count}} verzeichnis",
|
"folderWithCount_one": "{{count}} Verzeichnis",
|
||||||
"folderWithCount_other": "{{count}} verzeichnisse",
|
"folderWithCount_other": "{{count}} Verzeichnisse",
|
||||||
"albumArtist_one": "Albuminterpret",
|
"albumArtist_one": "Albuminterpret",
|
||||||
"albumArtist_other": "Albuminterpreten",
|
"albumArtist_other": "Albuminterpreten",
|
||||||
"track_one": "Track",
|
"track_one": "Track",
|
||||||
@@ -539,19 +543,19 @@
|
|||||||
"selectServer": "Server auswählen",
|
"selectServer": "Server auswählen",
|
||||||
"version": "Version {{version}}",
|
"version": "Version {{version}}",
|
||||||
"manageServers": "Server verwalten",
|
"manageServers": "Server verwalten",
|
||||||
"expandSidebar": "Seitenleiste erweitern",
|
"expandSidebar": "Seitenleiste ausklappen",
|
||||||
"collapseSidebar": "Seitenleiste einklappen",
|
"collapseSidebar": "Seitenleiste einklappen",
|
||||||
"openBrowserDevtools": "Browser-entwicklungswerkzeuge öffnen",
|
"openBrowserDevtools": "Browser-entwicklungswerkzeuge öffnen",
|
||||||
"goBack": "Gehe zurück",
|
"goBack": "Gehe zurück",
|
||||||
"goForward": "Gehe vorwärts",
|
"goForward": "Gehe vorwärts",
|
||||||
"settings": "$t(common.setting, {\"count\": 2})",
|
"settings": "$t(common.setting, {\"count\": 2})",
|
||||||
"quit": "$t(common.quit)",
|
"quit": "$t(common.quit)",
|
||||||
"privateModeOff": "Privatmodus deaktivieren",
|
"privateModeOff": "Privaten Modus deaktivieren",
|
||||||
"privateModeOn": "Privatmodus aktivieren",
|
"privateModeOn": "Privaten Modus aktivieren",
|
||||||
"commandPalette": "Kommandopalette öffnen",
|
"commandPalette": "Kommandopalette öffnen",
|
||||||
"selectMusicFolder": "Musikordner wählen",
|
"selectMusicFolder": "Musikverzeichnis wählen",
|
||||||
"noMusicFolder": "Kein musikordner gewählt",
|
"noMusicFolder": "Kein Musikverzeichnis gewählt",
|
||||||
"multipleMusicFolders": "{{count}} musikordner ausgewählt"
|
"multipleMusicFolders": "{{count}} Musikverzeichnis ausgewählt"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"mostPlayed": "Meistgespielt",
|
"mostPlayed": "Meistgespielt",
|
||||||
@@ -678,9 +682,9 @@
|
|||||||
"topSongs": "Toplieder",
|
"topSongs": "Toplieder",
|
||||||
"relatedArtists": "Ähnliche $t(entity.artist, {\"count\": 2})",
|
"relatedArtists": "Ähnliche $t(entity.artist, {\"count\": 2})",
|
||||||
"groupingTypeAll": "Alle veröffentlichungsformate",
|
"groupingTypeAll": "Alle veröffentlichungsformate",
|
||||||
"groupingTypePrimary": "Primäre veröffentlichungsformate",
|
"groupingTypePrimary": "Primäre Veröffentlichungsformate",
|
||||||
"favoriteSongs": "Lieblingssongs",
|
"favoriteSongs": "Lieblingslieder",
|
||||||
"favoriteSongsFrom": "Liebslingssongs von {{title}}",
|
"favoriteSongsFrom": "Liebslingslieder von {{title}}",
|
||||||
"topSongsCommunity": "Community",
|
"topSongsCommunity": "Community",
|
||||||
"topSongsPersonal": "Persönlich"
|
"topSongsPersonal": "Persönlich"
|
||||||
},
|
},
|
||||||
@@ -711,7 +715,7 @@
|
|||||||
},
|
},
|
||||||
"windowBar": {
|
"windowBar": {
|
||||||
"paused": "(Pausiert) ",
|
"paused": "(Pausiert) ",
|
||||||
"privateMode": "(Privater modus)"
|
"privateMode": "(Privater Modus)"
|
||||||
},
|
},
|
||||||
"collections": {
|
"collections": {
|
||||||
"saveAsCollection": "Als sammlung speichern",
|
"saveAsCollection": "Als sammlung speichern",
|
||||||
@@ -758,8 +762,8 @@
|
|||||||
"addLastShuffled": "Als Letztes (zufällige Wiedergabe)",
|
"addLastShuffled": "Als Letztes (zufällige Wiedergabe)",
|
||||||
"addNextShuffled": "Als Nächstes (zufällige Wiedergabe)",
|
"addNextShuffled": "Als Nächstes (zufällige Wiedergabe)",
|
||||||
"holdToShuffle": "Halten für zufallswiedergabe",
|
"holdToShuffle": "Halten für zufallswiedergabe",
|
||||||
"restoreQueueFromServer": "Wiedergabeliste von server wiederherstellen",
|
"restoreQueueFromServer": "Wiedergabeliste von Server wiederherstellen",
|
||||||
"saveQueueToServer": "Wiedergabeliste auf server speichern",
|
"saveQueueToServer": "Wiedergabeliste auf Server speichern",
|
||||||
"lyrics": "Songtexte",
|
"lyrics": "Songtexte",
|
||||||
"artistRadio": "Künstler radio",
|
"artistRadio": "Künstler radio",
|
||||||
"sleepTimer_endOfSong": "Ende des aktuellen liedes",
|
"sleepTimer_endOfSong": "Ende des aktuellen liedes",
|
||||||
@@ -897,13 +901,13 @@
|
|||||||
"sidebarPlaylistSorting": "Wiedergabelisten-sortierung in der seitenleiste",
|
"sidebarPlaylistSorting": "Wiedergabelisten-sortierung in der seitenleiste",
|
||||||
"minimizeToTray": "Zur taskleiste minimieren",
|
"minimizeToTray": "Zur taskleiste minimieren",
|
||||||
"skipPlaylistPage": "Wiedergabeliste-seite überspringen",
|
"skipPlaylistPage": "Wiedergabeliste-seite überspringen",
|
||||||
"themeDark": "Erscheinungsbild (dunkel)",
|
"themeDark": "Design (dunkel)",
|
||||||
"sidebarCollapsedNavigation": "Navigation in der seitenleiste (komprimiert)",
|
"sidebarCollapsedNavigation": "Navigation in der seitenleiste (komprimiert)",
|
||||||
"gaplessAudio_optionWeak": "Schwach (empfohlen)",
|
"gaplessAudio_optionWeak": "Schwach (empfohlen)",
|
||||||
"minimumScrobbleSeconds": "Minimum scrobble-dauer (sekunden)",
|
"minimumScrobbleSeconds": "Minimum scrobble-dauer (sekunden)",
|
||||||
"hotkey_playbackStop": "Stoppen",
|
"hotkey_playbackStop": "Stoppen",
|
||||||
"savePlayQueue_description": "Speichert die Wiedergabeliste beim Schließen der Anwendung, und stellt diese wieder her, wenn die Anwendung geöffnet wird",
|
"savePlayQueue_description": "Speichert die Wiedergabeliste beim Schließen der Anwendung, und stellt diese wieder her, wenn die Anwendung geöffnet wird",
|
||||||
"useSystemTheme": "Nach erscheinungsbild des systems richten",
|
"useSystemTheme": "Nach Erscheinungsbild des Systems richten",
|
||||||
"enableRemote_description": "Aktiviert den Server für die Fernsteuerung, damit andere Geräte die Anwendung steuern können",
|
"enableRemote_description": "Aktiviert den Server für die Fernsteuerung, damit andere Geräte die Anwendung steuern können",
|
||||||
"fontType_optionSystem": "System schriftart",
|
"fontType_optionSystem": "System schriftart",
|
||||||
"discordUpdateInterval": "{{discord}} rich presence aktualisierungsintervall",
|
"discordUpdateInterval": "{{discord}} rich presence aktualisierungsintervall",
|
||||||
@@ -919,7 +923,7 @@
|
|||||||
"fontType": "Schriftartenquelle",
|
"fontType": "Schriftartenquelle",
|
||||||
"followLyric": "Aktuellen songtext synchronisieren",
|
"followLyric": "Aktuellen songtext synchronisieren",
|
||||||
"font_description": "Wähle die Schriftart für die Anwendung",
|
"font_description": "Wähle die Schriftart für die Anwendung",
|
||||||
"themeLight": "Erscheinungsbild (hell)",
|
"themeLight": "Design (hell)",
|
||||||
"sidePlayQueueStyle_optionDetached": "Lösgelöst",
|
"sidePlayQueueStyle_optionDetached": "Lösgelöst",
|
||||||
"windowBarStyle_description": "Legt das Erscheinungsbild des Fensterrahmens fest",
|
"windowBarStyle_description": "Legt das Erscheinungsbild des Fensterrahmens fest",
|
||||||
"hotkey_toggleCurrentSongFavorite": "$t(common.currentSong) zu favoriten hinzufügen",
|
"hotkey_toggleCurrentSongFavorite": "$t(common.currentSong) zu favoriten hinzufügen",
|
||||||
@@ -947,7 +951,7 @@
|
|||||||
"albumBackgroundBlur_description": "Passt die Stärke der Unschärfe an, welche auf das Hintergrundbild des Albums angewandt wird",
|
"albumBackgroundBlur_description": "Passt die Stärke der Unschärfe an, welche auf das Hintergrundbild des Albums angewandt wird",
|
||||||
"clearCacheSuccess": "Cache erfolgreich geleert",
|
"clearCacheSuccess": "Cache erfolgreich geleert",
|
||||||
"contextMenu": "Kontextmenü-einstellungen (rechtsklick)",
|
"contextMenu": "Kontextmenü-einstellungen (rechtsklick)",
|
||||||
"customCssEnable_description": "Erlaubt das hinzufügen von benutzerdefiniertem CSS",
|
"customCssEnable_description": "Erlaubt das Hinzufügen von benutzerdefiniertem CSS",
|
||||||
"artistBackground": "Künstler hintergrundbild",
|
"artistBackground": "Künstler hintergrundbild",
|
||||||
"artistBackground_description": "Fügt ein Hintergrundbild für die Künstlerseite hinzu",
|
"artistBackground_description": "Fügt ein Hintergrundbild für die Künstlerseite hinzu",
|
||||||
"artistConfiguration": "Künstler albumseite konfiguration",
|
"artistConfiguration": "Künstler albumseite konfiguration",
|
||||||
@@ -975,11 +979,10 @@
|
|||||||
"logLevel_optionError": "Fehler",
|
"logLevel_optionError": "Fehler",
|
||||||
"logLevel_optionInfo": "Info",
|
"logLevel_optionInfo": "Info",
|
||||||
"logLevel_optionWarn": "Warnung",
|
"logLevel_optionWarn": "Warnung",
|
||||||
"autoDJ_description": "Füge automatisch ähnliche Lieder der Wiedergabeliste hinzu",
|
|
||||||
"autoDJ": "Auto DJ",
|
"autoDJ": "Auto DJ",
|
||||||
"autoDJ_itemCount": "Anzahl",
|
"autoDJ_itemCount": "Anzahl",
|
||||||
"autoDJ_itemCount_description": "Die anzahl der lieder, die bei aktiviertem auto DJ zur wiedergabeliste hinzugefügt werden sollen",
|
"autoDJ_itemCount_description": "Die Anzahl der Lieder, die zur Wiedergabeliste hinzugefügt werden soll",
|
||||||
"autoDJ_timing_description": "Die anzahl der lieder, die sich noch in der wiedergabeliste befinden, bevor auto DJ ausgelöst wird",
|
"autoDJ_timing_description": "Die Anzahl der Lieder, die sich noch in der Wiedergabeliste befinden, bevor Auto-DJ ausgelöst wird",
|
||||||
"autoDJ_timing": "Timing",
|
"autoDJ_timing": "Timing",
|
||||||
"discordDisplayType": "{{discord}} presence darstellungsart",
|
"discordDisplayType": "{{discord}} presence darstellungsart",
|
||||||
"discordLinkType_mbz_lastfm": "{{musicbrainz}} mit {{lastfm}} als ersatz",
|
"discordLinkType_mbz_lastfm": "{{musicbrainz}} mit {{lastfm}} als ersatz",
|
||||||
@@ -1020,7 +1023,7 @@
|
|||||||
"artistConfiguration_description": "Legt fest, welche Elemente auf der Albumkünstlerseite angezeigt werden und in welcher Reihenfolge",
|
"artistConfiguration_description": "Legt fest, welche Elemente auf der Albumkünstlerseite angezeigt werden und in welcher Reihenfolge",
|
||||||
"contextMenu_description": "Legt die Einträge fest, die im Rechtsklick-Menü angezeigt werden sollen. Abgewählte Einträge werden ausgeblendet",
|
"contextMenu_description": "Legt die Einträge fest, die im Rechtsklick-Menü angezeigt werden sollen. Abgewählte Einträge werden ausgeblendet",
|
||||||
"crossfadeStyle": "Art der überblende",
|
"crossfadeStyle": "Art der überblende",
|
||||||
"customCss_description": "Benutzerdefinierter CSS-inhalt. Hinweis: inhalte und remote-urls sind nicht zulässige eigenschaften. Unten siehst du eine vorschau deines inhalts. Aufgrund von bereinigung werden womöglich zusätzliche, nicht von dir definierte felder angezeigt",
|
"customCss_description": "Benutzerdefinierter CSS-Inhalt. Hinweis: Content und Remote URLs sind nicht zulässige Eigenschaften. Eine Vorschau deines Inhalts wird unten angezeigt. Aufgrund von Bereinigung werden womöglich zusätzliche, nicht von dir definierte Felder angezeigt. Desktop: Feishin liest und schreibt in eine custom.css Datei im App-Konfigurationsverzeichnis, und lädt diese neu, wenn sich die Datei ändert.",
|
||||||
"customCssNotice": "Warnung: obwohl eine gewisse bereinigung erfolgt (nicht zulässig sind z. B. \"URL()\" und \"content:\"), kann ein benutzerdefiniertes CSS risiken mit sich bringen, da die benutzeroberfläche dadurch verändert wird",
|
"customCssNotice": "Warnung: obwohl eine gewisse bereinigung erfolgt (nicht zulässig sind z. B. \"URL()\" und \"content:\"), kann ein benutzerdefiniertes CSS risiken mit sich bringen, da die benutzeroberfläche dadurch verändert wird",
|
||||||
"releaseChannel_optionBeta": "Beta",
|
"releaseChannel_optionBeta": "Beta",
|
||||||
"releaseChannel_optionLatest": "Stabil",
|
"releaseChannel_optionLatest": "Stabil",
|
||||||
@@ -1062,7 +1065,7 @@
|
|||||||
"automaticUpdates": "Automatische updates",
|
"automaticUpdates": "Automatische updates",
|
||||||
"automaticUpdates_description": "Updates automatisch suchen und installieren",
|
"automaticUpdates_description": "Updates automatisch suchen und installieren",
|
||||||
"releaseChannel_optionAlpha": "Alpha (nightly)",
|
"releaseChannel_optionAlpha": "Alpha (nightly)",
|
||||||
"useThemeAccentColor": "Akzentfarbe des themas nutzen",
|
"useThemeAccentColor": "Standard Akzentfarbe übernehmen",
|
||||||
"analyticsEnable_description": "Anonymisierte Nutzungsdaten werden an den Entwickler gesendet, um die Anwendung zu verbessern",
|
"analyticsEnable_description": "Anonymisierte Nutzungsdaten werden an den Entwickler gesendet, um die Anwendung zu verbessern",
|
||||||
"artistReleaseTypeConfiguration_description": "Konfigurieren, welche Release-Typen und in welcher Reihenfolge diese auf der Album-Künstlerseite angezeigt werden",
|
"artistReleaseTypeConfiguration_description": "Konfigurieren, welche Release-Typen und in welcher Reihenfolge diese auf der Album-Künstlerseite angezeigt werden",
|
||||||
"homeConfiguration_description": "Konfigurieren, welche Elemente und in welcher Reihenfolge diese auf der Startseite angezeigt werden",
|
"homeConfiguration_description": "Konfigurieren, welche Elemente und in welcher Reihenfolge diese auf der Startseite angezeigt werden",
|
||||||
@@ -1109,14 +1112,14 @@
|
|||||||
"queryBuilder": "Abfrage-editor",
|
"queryBuilder": "Abfrage-editor",
|
||||||
"queryBuilderCustomFields_inputLabel": "Label",
|
"queryBuilderCustomFields_inputLabel": "Label",
|
||||||
"queryBuilderCustomFields_description": "Füge benutzerdefinierte Felder für den Abfrage-Editor hinzu",
|
"queryBuilderCustomFields_description": "Füge benutzerdefinierte Felder für den Abfrage-Editor hinzu",
|
||||||
"autosave": "Automatisch aktuelle wiedergabeliste speichern",
|
"autosave": "Automatisch aktuelle Wiedergabeliste speichern",
|
||||||
"autosave_description": "Aktiviere die automatische speicherung der aktuellen wiedergabe auf dem server. Diese funktion ist nur bei Navidrome/Subsonic servern verfügbar und es darf sich nicht um eine gemischte wiedergabeliste handeln.",
|
"autosave_description": "Aktiviere die automatische Speicherung der aktuellen Wiedergabe auf dem Server. Diese Funktion ist nur bei Navidrome/Subsonic Servern verfügbar und es darf sich nicht um eine gemischte Wiedergabeliste handeln.",
|
||||||
"autosaveCount": "Häufigkeit der automatischen speicherung bei wiedergabelisten",
|
"autosaveCount": "Häufigkeit der automatischen Speicherung bei Wiedergabelisten",
|
||||||
"autosaveCount_description": "Wieviele Lieder gespielt werden, bevor die Wiedergabeliste gespeichert wird. 1 (Minimum) bedeutet die Speicherung nach jedem gespielten Lied",
|
"autosaveCount_description": "Wieviele Lieder gespielt werden, bevor die Wiedergabeliste gespeichert wird. 1 (Minimum) bedeutet die Speicherung nach jedem gespielten Lied",
|
||||||
"useThemeAccentColor_description": "Verwendet die Primärfarbe des gewählten Themas anstatt einer ausgewählten Akzentfarbe",
|
"useThemeAccentColor_description": "Verwendet die primäre Farbe des gewählten Designs",
|
||||||
"useThemePrimaryShade": "Primärschatten des themas nutzen",
|
"useThemePrimaryShade": "Standard Farbton übernehmen",
|
||||||
"useThemePrimaryShade_description": "Verwendet den Primärschatten des ausgewählten Themas als primäre Farbvarianten",
|
"useThemePrimaryShade_description": "Verwendet den primären Farbton des ausgewählten Designs für die Primärfarbvarianten",
|
||||||
"primaryShade": "Primärschatten",
|
"primaryShade": "Primärer Farbton",
|
||||||
"listenbrainz": "ListenBrainz Links anzeigen",
|
"listenbrainz": "ListenBrainz Links anzeigen",
|
||||||
"listenbrainz_description": "Zeige Links zu ListenBrainz auf den Interpreten/Alben Seiten",
|
"listenbrainz_description": "Zeige Links zu ListenBrainz auf den Interpreten/Alben Seiten",
|
||||||
"mpvExtraParameters": "Zusätzliche mpv parameter",
|
"mpvExtraParameters": "Zusätzliche mpv parameter",
|
||||||
@@ -1131,7 +1134,15 @@
|
|||||||
"nativeSpotify_description": "In der Spotify app statt im browser öffnen",
|
"nativeSpotify_description": "In der Spotify app statt im browser öffnen",
|
||||||
"imageResolution_optionFullScreenPlayer": "Wiedergabe im vollbildmodus",
|
"imageResolution_optionFullScreenPlayer": "Wiedergabe im vollbildmodus",
|
||||||
"sidePlayQueueLayout_optionHorizontal": "Horizontal",
|
"sidePlayQueueLayout_optionHorizontal": "Horizontal",
|
||||||
"sidePlayQueueLayout_optionVertical": "Vertikal"
|
"sidePlayQueueLayout_optionVertical": "Vertikal",
|
||||||
|
"sidebarPlaylistFolders": "Verzeichnisse aktivieren",
|
||||||
|
"sidebarPlaylistFolderSeparator": "Verzeichnistrennzeichen",
|
||||||
|
"sidebarPlaylistFolderView_description": "Wie Verzeichnisse in der Seitenleiste angezeigt werden",
|
||||||
|
"sidebarPlaylistFolderView": "Verzeichnisansicht",
|
||||||
|
"sidebarPlaylistFolderView_optionSingle": "Einzelne Ordner",
|
||||||
|
"sidebarPlaylistFolderView_optionTree": "Baumstruktur",
|
||||||
|
"sidebarPlaylistFolderView_optionNavigation": "Navigationsansicht",
|
||||||
|
"sidebarPlaylistFolderSeparator_description": "Zeichen (oder Zeichenfolge), das die Verzeichnisebenen im Wiedergabelistentitel trennt"
|
||||||
},
|
},
|
||||||
"dragDropZone": {
|
"dragDropZone": {
|
||||||
"error_oneFileOnly": "Bitte wähle nur 1 Datei",
|
"error_oneFileOnly": "Bitte wähle nur 1 Datei",
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
"expand": "Expand",
|
"expand": "Expand",
|
||||||
"example": "Example",
|
"example": "Example",
|
||||||
"externalLinks": "External links",
|
"externalLinks": "External links",
|
||||||
|
"openFolder": "Open folder",
|
||||||
"faster": "Faster",
|
"faster": "Faster",
|
||||||
"favorite": "Favorite",
|
"favorite": "Favorite",
|
||||||
"filter_one": "Filter",
|
"filter_one": "Filter",
|
||||||
@@ -415,6 +416,11 @@
|
|||||||
},
|
},
|
||||||
"shuffleAll": {
|
"shuffleAll": {
|
||||||
"title": "Play random",
|
"title": "Play random",
|
||||||
|
"input_kind_albums": "Albums",
|
||||||
|
"input_kind_songs": "Songs",
|
||||||
|
"input_kind": "Random picks",
|
||||||
|
"input_limit_albums": "How many albums?",
|
||||||
|
"input_limit_songs": "How many songs?",
|
||||||
"input_genre": "$t(entity.genre, {\"count\": 1})",
|
"input_genre": "$t(entity.genre, {\"count\": 1})",
|
||||||
"input_limit": "How many songs?",
|
"input_limit": "How many songs?",
|
||||||
"input_minYear": "From year",
|
"input_minYear": "From year",
|
||||||
@@ -731,11 +737,19 @@
|
|||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"autoDJ": "Auto DJ",
|
"autoDJ": "Auto DJ",
|
||||||
"autoDJ_description": "Automatically add similar songs to the queue",
|
|
||||||
"autoDJ_itemCount": "Item count",
|
"autoDJ_itemCount": "Item count",
|
||||||
"autoDJ_itemCount_description": "The number of items attempted to be added to the queue when auto DJ is enabled",
|
"autoDJ_itemCount_description": "The number of items attempted to be added to the queue",
|
||||||
"autoDJ_timing": "Timing",
|
"autoDJ_timing": "Timing",
|
||||||
"autoDJ_timing_description": "The number of songs remaining in the queue before auto DJ is triggered",
|
"autoDJ_timing_description": "The number of songs remaining in the queue before auto DJ is triggered",
|
||||||
|
"autoDJ_mode": "Mode",
|
||||||
|
"autoDJ_mode_albums": "Albums",
|
||||||
|
"autoDJ_mode_description": "Choose to add either songs or entire albums to the queue",
|
||||||
|
"autoDJ_mode_songs": "Songs",
|
||||||
|
"autoDJ_enabled": "Enable Auto DJ",
|
||||||
|
"autoDJ_albumStrategy": "Album selection mode",
|
||||||
|
"autoDJ_songStrategy": "Song selection mode",
|
||||||
|
"autoDJ_strategy_option_library_random": "Random",
|
||||||
|
"autoDJ_strategy_option_similar": "Similar",
|
||||||
"autosave": "Automatically save play queue",
|
"autosave": "Automatically save play queue",
|
||||||
"autosave_description": "Enable automatically saving the play queue to your server. This is only possible when using Navidrome/Subsonic, and you cannot have a mixed play queue.",
|
"autosave_description": "Enable automatically saving the play queue to your server. This is only possible when using Navidrome/Subsonic, and you cannot have a mixed play queue.",
|
||||||
"autosaveCount": "Automatic play queue save frequency",
|
"autosaveCount": "Automatic play queue save frequency",
|
||||||
@@ -785,7 +799,7 @@
|
|||||||
"crossfadeDuration": "Crossfade duration",
|
"crossfadeDuration": "Crossfade duration",
|
||||||
"crossfadeStyle": "Crossfade style",
|
"crossfadeStyle": "Crossfade style",
|
||||||
"crossfadeStyle_description": "Select the crossfade style to use for the audio player",
|
"crossfadeStyle_description": "Select the crossfade style to use for the audio player",
|
||||||
"customCss_description": "Custom CSS content. Note: content and remote urls are disallowed properties. A preview of your content is shown below. Additional fields you didn't set are present due to sanitization",
|
"customCss_description": "Custom CSS content. Note: content and remote urls are disallowed properties. A preview of your content is shown below. Additional fields you didn't set are present due to sanitization. Desktop: feishin reads and writes custom.css in the app config directory and reloads it when the file changes",
|
||||||
"customCss": "Custom CSS",
|
"customCss": "Custom CSS",
|
||||||
"customCssEnable_description": "Allow for writing custom CSS",
|
"customCssEnable_description": "Allow for writing custom CSS",
|
||||||
"customCssEnable": "Enable custom CSS",
|
"customCssEnable": "Enable custom CSS",
|
||||||
|
|||||||
@@ -235,7 +235,7 @@
|
|||||||
"customCssEnable_description": "Permite escribir CSS personalizado",
|
"customCssEnable_description": "Permite escribir CSS personalizado",
|
||||||
"customCss": "CSS personalizado",
|
"customCss": "CSS personalizado",
|
||||||
"customCssNotice": "Aviso: mientras hay alguna sanitización (rechazar URL() y content:), usar CSS personalizado puede aún entrañar riesgos cambiando la interfaz",
|
"customCssNotice": "Aviso: mientras hay alguna sanitización (rechazar URL() y content:), usar CSS personalizado puede aún entrañar riesgos cambiando la interfaz",
|
||||||
"customCss_description": "Content CSS personalizado. Nota: content y urls remotas son propiedades rechazadas. Una vista previa de tu content se muestra debajo. Las entradas adicionales que no estableciste están presentes debido a la sanitización",
|
"customCss_description": "Content CSS personalizado. Nota: content y remote urls son propiedades rechazadas. Una vista previa de tu content se muestra debajo. Las entradas adicionales que no estableciste están presentes debido a la sanitización. Escritorio: Feishin lee y escribe custom.css en el directorio de configuración de la aplicación y lo recarga cuando cambia el archivo",
|
||||||
"webAudio": "Usar audio web",
|
"webAudio": "Usar audio web",
|
||||||
"webAudio_description": "Utilizar audio web. Esto habilita funciones avanzadas como ReplayGain. Desactiva esta opción si tienes problemas",
|
"webAudio_description": "Utilizar audio web. Esto habilita funciones avanzadas como ReplayGain. Desactiva esta opción si tienes problemas",
|
||||||
"transcode_description": "Permite la transcodificación a distintos formatos",
|
"transcode_description": "Permite la transcodificación a distintos formatos",
|
||||||
@@ -344,9 +344,8 @@
|
|||||||
"playerFilters_description": "Omite la adición de canciones a la cola basado en los siguientes criterios",
|
"playerFilters_description": "Omite la adición de canciones a la cola basado en los siguientes criterios",
|
||||||
"playerbarSlider_description": "La forma de onda no es recomendable en una conexión a Internet lenta o medida",
|
"playerbarSlider_description": "La forma de onda no es recomendable en una conexión a Internet lenta o medida",
|
||||||
"autoDJ": "DJ Automático",
|
"autoDJ": "DJ Automático",
|
||||||
"autoDJ_description": "Añade canciones similares a las de la cola automáticamente",
|
|
||||||
"autoDJ_itemCount": "Recuento de elementos",
|
"autoDJ_itemCount": "Recuento de elementos",
|
||||||
"autoDJ_itemCount_description": "El número de elementos que se ha intentado añadir a la cola cuando DJ automático está activado",
|
"autoDJ_itemCount_description": "El número de elementos que se ha intentado añadir a la cola",
|
||||||
"autoDJ_timing_description": "El número de canciones restantes en la cola antes de que DJ automático se dispare",
|
"autoDJ_timing_description": "El número de canciones restantes en la cola antes de que DJ automático se dispare",
|
||||||
"autoDJ_timing": "Tiempo",
|
"autoDJ_timing": "Tiempo",
|
||||||
"logLevel": "Nivel de registro",
|
"logLevel": "Nivel de registro",
|
||||||
@@ -448,7 +447,16 @@
|
|||||||
"sidebarPlaylistMode_optionCompact": "Compacto",
|
"sidebarPlaylistMode_optionCompact": "Compacto",
|
||||||
"sidebarPlaylistMode_optionExpanded": "Expandido",
|
"sidebarPlaylistMode_optionExpanded": "Expandido",
|
||||||
"sidebarPlaylistMode_description": "Cómo se muestra cada lista de reproducción en la lista de la barra lateral",
|
"sidebarPlaylistMode_description": "Cómo se muestra cada lista de reproducción en la lista de la barra lateral",
|
||||||
"sidebarPlaylistFolderTreeIndent_description": "Píxeles que está sangrado cada nivel del árbol"
|
"sidebarPlaylistFolderTreeIndent_description": "Píxeles que está sangrado cada nivel del árbol",
|
||||||
|
"autoDJ_mode": "Modo",
|
||||||
|
"autoDJ_mode_albums": "Álbumes",
|
||||||
|
"autoDJ_mode_songs": "Canciones",
|
||||||
|
"autoDJ_enabled": "Activar DJ automático",
|
||||||
|
"autoDJ_albumStrategy": "Modo de selección de álbum",
|
||||||
|
"autoDJ_songStrategy": "Modo de selección de canción",
|
||||||
|
"autoDJ_strategy_option_library_random": "Aleatorio",
|
||||||
|
"autoDJ_strategy_option_similar": "Similar",
|
||||||
|
"autoDJ_mode_description": "Elegir para añadir canciones o álbumes enteros a la cola"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"editPlaylist": "Editar $t(entity.playlist, {\"count\": 1})",
|
"editPlaylist": "Editar $t(entity.playlist, {\"count\": 1})",
|
||||||
@@ -624,7 +632,8 @@
|
|||||||
"newVersionAvailable": "Una nueva versión está disponible",
|
"newVersionAvailable": "Una nueva versión está disponible",
|
||||||
"numberOfResults": "{{numberOfResults}} resultados",
|
"numberOfResults": "{{numberOfResults}} resultados",
|
||||||
"grouping": "Agrupar",
|
"grouping": "Agrupar",
|
||||||
"back": "Atrás"
|
"back": "Atrás",
|
||||||
|
"openFolder": "Abrir carpeta"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "Reiniciar el servidor para aplicar el nuevo puerto",
|
"remotePortWarning": "Reiniciar el servidor para aplicar el nuevo puerto",
|
||||||
@@ -1014,7 +1023,12 @@
|
|||||||
"input_played": "Reproducir filtro",
|
"input_played": "Reproducir filtro",
|
||||||
"input_played_optionAll": "Todas las pistas",
|
"input_played_optionAll": "Todas las pistas",
|
||||||
"input_played_optionUnplayed": "Solo las pistas sin reproducir",
|
"input_played_optionUnplayed": "Solo las pistas sin reproducir",
|
||||||
"input_played_optionPlayed": "Solo las pistas reproducidas"
|
"input_played_optionPlayed": "Solo las pistas reproducidas",
|
||||||
|
"input_kind_albums": "Álbumes",
|
||||||
|
"input_kind_songs": "Canciones",
|
||||||
|
"input_limit_albums": "¿Cuántos álbumes?",
|
||||||
|
"input_limit_songs": "¿Cuántas canciones?",
|
||||||
|
"input_kind": "Selecciones aleatorias"
|
||||||
},
|
},
|
||||||
"saveQueue": {
|
"saveQueue": {
|
||||||
"success": "Cola de reproducción guardada en el servidor"
|
"success": "Cola de reproducción guardada en el servidor"
|
||||||
|
|||||||
@@ -658,7 +658,6 @@
|
|||||||
"transcodeFormat": "Transkodetzeko formatua",
|
"transcodeFormat": "Transkodetzeko formatua",
|
||||||
"queryBuilderCustomFields_inputLabel": "Etiketa",
|
"queryBuilderCustomFields_inputLabel": "Etiketa",
|
||||||
"autoDJ": "DJ automatikoa",
|
"autoDJ": "DJ automatikoa",
|
||||||
"autoDJ_description": "Automatikoki gehitu antzeko abestiak ilaran",
|
|
||||||
"autoDJ_itemCount_description": "DJ automatikoa gaituta dagoenean ilaran gehitzen saiatu diren elementuen kopurua",
|
"autoDJ_itemCount_description": "DJ automatikoa gaituta dagoenean ilaran gehitzen saiatu diren elementuen kopurua",
|
||||||
"autoDJ_timing_description": "DJ automatikoa aktibatu aurretik ilaran geratzen diren abestien kopurua",
|
"autoDJ_timing_description": "DJ automatikoa aktibatu aurretik ilaran geratzen diren abestien kopurua",
|
||||||
"analyticsDisable": "Erabileran oinarritutako analisiei uko egin",
|
"analyticsDisable": "Erabileran oinarritutako analisiei uko egin",
|
||||||
|
|||||||
@@ -630,7 +630,6 @@
|
|||||||
"releaseChannel_description": "Valitse vakaiden ja beetaversioiden välillä automaattisille päivityksille",
|
"releaseChannel_description": "Valitse vakaiden ja beetaversioiden välillä automaattisille päivityksille",
|
||||||
"discordDisplayType_artistname": "Artistin nimi / artistien nimet",
|
"discordDisplayType_artistname": "Artistin nimi / artistien nimet",
|
||||||
"autoDJ": "Auto DJ",
|
"autoDJ": "Auto DJ",
|
||||||
"autoDJ_description": "Lisää automaattisesti samanlaisia kappaleita jonoon",
|
|
||||||
"autoDJ_itemCount": "Kohteiden määrä",
|
"autoDJ_itemCount": "Kohteiden määrä",
|
||||||
"autoDJ_itemCount_description": "Jonoon lisättäväksi yritettyjen kohteiden määrä, kun auto DJ on käytössä",
|
"autoDJ_itemCount_description": "Jonoon lisättäväksi yritettyjen kohteiden määrä, kun auto DJ on käytössä",
|
||||||
"autoDJ_timing": "Ajastus"
|
"autoDJ_timing": "Ajastus"
|
||||||
|
|||||||
@@ -812,7 +812,6 @@
|
|||||||
"queryBuilderCustomFields": "Champs personnalisé",
|
"queryBuilderCustomFields": "Champs personnalisé",
|
||||||
"queryBuilderCustomFields_description": "Ajouter des champs personnalisés à utiliser dans les constructeurs de requêtes",
|
"queryBuilderCustomFields_description": "Ajouter des champs personnalisés à utiliser dans les constructeurs de requêtes",
|
||||||
"autoDJ": "DJ auto",
|
"autoDJ": "DJ auto",
|
||||||
"autoDJ_description": "Ajouter automatiquement des titres similaire à la file d'attente",
|
|
||||||
"autoDJ_itemCount": "Nombre d'entrée",
|
"autoDJ_itemCount": "Nombre d'entrée",
|
||||||
"autoDJ_itemCount_description": "Le nombre d'entrées tentées d'être ajoutées à la file d'attente lorsque le DJ auto est activé",
|
"autoDJ_itemCount_description": "Le nombre d'entrées tentées d'être ajoutées à la file d'attente lorsque le DJ auto est activé",
|
||||||
"autoDJ_timing": "Timing",
|
"autoDJ_timing": "Timing",
|
||||||
|
|||||||
@@ -917,7 +917,6 @@
|
|||||||
"queryBuilderCustomFields_description": "Egyéni mezők hozzáadása a lekérdezés-építőhöz",
|
"queryBuilderCustomFields_description": "Egyéni mezők hozzáadása a lekérdezés-építőhöz",
|
||||||
"autoDJ": "Auto DJ",
|
"autoDJ": "Auto DJ",
|
||||||
"autoDJ_timing": "Időzítés",
|
"autoDJ_timing": "Időzítés",
|
||||||
"autoDJ_description": "Hasonló dalokat automatikusan hozzáad a műsorlistához",
|
|
||||||
"autoDJ_itemCount": "Elem szám",
|
"autoDJ_itemCount": "Elem szám",
|
||||||
"autoDJ_itemCount_description": "Az auto DJ engedélyezésekor a műsorsorba felvenni kívánt elemek száma",
|
"autoDJ_itemCount_description": "Az auto DJ engedélyezésekor a műsorsorba felvenni kívánt elemek száma",
|
||||||
"autoDJ_timing_description": "Az auto DJ elindulása előtt a műsorlistában maradt dalok száma",
|
"autoDJ_timing_description": "Az auto DJ elindulása előtt a műsorlistában maradt dalok száma",
|
||||||
|
|||||||
+126
-63
@@ -16,7 +16,10 @@
|
|||||||
"viewPlaylists": "Lihat $t(entity.playlist, {\"count\": 2})",
|
"viewPlaylists": "Lihat $t(entity.playlist, {\"count\": 2})",
|
||||||
"openIn": {
|
"openIn": {
|
||||||
"lastfm": "Buka di Last.fm",
|
"lastfm": "Buka di Last.fm",
|
||||||
"musicbrainz": "Buka di MusicBrainz"
|
"musicbrainz": "Buka di MusicBrainz",
|
||||||
|
"listenbrainz": "Buka di ListenBrainz",
|
||||||
|
"qobuz": "Buka di Qobuz",
|
||||||
|
"spotify": "Buka di Spotify"
|
||||||
},
|
},
|
||||||
"addToFavorites": "Tambahkan ke $t(entity.favorite, {\"count\": 2})",
|
"addToFavorites": "Tambahkan ke $t(entity.favorite, {\"count\": 2})",
|
||||||
"clearQueue": "Kosongkan antrian",
|
"clearQueue": "Kosongkan antrian",
|
||||||
@@ -38,12 +41,14 @@
|
|||||||
"shuffleSelected": "Acak yang dipilih",
|
"shuffleSelected": "Acak yang dipilih",
|
||||||
"viewMore": "Lihat lebih banyak",
|
"viewMore": "Lihat lebih banyak",
|
||||||
"openApplicationDirectory": "Buka direktori aplikasi",
|
"openApplicationDirectory": "Buka direktori aplikasi",
|
||||||
"goToCurrent": "Pergi ke item saat ini"
|
"goToCurrent": "Pergi ke item saat ini",
|
||||||
|
"collapseAllFolders": "Ciutkan semua folder",
|
||||||
|
"expandAllFolders": "Bentangkan semua folder"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"clear": "Bersihkan",
|
"clear": "Bersihkan",
|
||||||
"action_other": "Aksi",
|
"action_other": "Aksi",
|
||||||
"codec": "Koded",
|
"codec": "Kodek",
|
||||||
"channel_other": "Saluran",
|
"channel_other": "Saluran",
|
||||||
"duration": "Durasi",
|
"duration": "Durasi",
|
||||||
"create": "Buat",
|
"create": "Buat",
|
||||||
@@ -96,7 +101,7 @@
|
|||||||
"random": "Acak",
|
"random": "Acak",
|
||||||
"rating": "Penilaian",
|
"rating": "Penilaian",
|
||||||
"refresh": "Segarkan",
|
"refresh": "Segarkan",
|
||||||
"reload": "Muat Ulang",
|
"reload": "Muat ulang",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"resetToDefault": "Reset ke default",
|
"resetToDefault": "Reset ke default",
|
||||||
"restartRequired": "Restart diperlukan",
|
"restartRequired": "Restart diperlukan",
|
||||||
@@ -111,7 +116,7 @@
|
|||||||
"sortOrder": "Urutkan",
|
"sortOrder": "Urutkan",
|
||||||
"title": "Judul",
|
"title": "Judul",
|
||||||
"trackNumber": "Pista",
|
"trackNumber": "Pista",
|
||||||
"trackGain": "Gain pista",
|
"trackGain": "Gain trek",
|
||||||
"trackPeak": "Puncak lagu",
|
"trackPeak": "Puncak lagu",
|
||||||
"unknown": "Tidak dikenal",
|
"unknown": "Tidak dikenal",
|
||||||
"version": "Versi",
|
"version": "Versi",
|
||||||
@@ -156,7 +161,11 @@
|
|||||||
"clean": "Bersih",
|
"clean": "Bersih",
|
||||||
"gridRows": "Baris kisi",
|
"gridRows": "Baris kisi",
|
||||||
"tableColumns": "Kolom tabel",
|
"tableColumns": "Kolom tabel",
|
||||||
"itemsMore": "{{count}} lagi"
|
"itemsMore": "{{count}} lagi",
|
||||||
|
"back": "Kembali",
|
||||||
|
"grouping": "Pengelompokan",
|
||||||
|
"numberOfResults": "{{numberOfResults}} hasil",
|
||||||
|
"newVersionAvailable": "Versi baru tersedia"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"album_other": "Album",
|
"album_other": "Album",
|
||||||
@@ -173,7 +182,7 @@
|
|||||||
"playlist_other": "Daftar Putar",
|
"playlist_other": "Daftar Putar",
|
||||||
"play_other": "Putar {{count}}",
|
"play_other": "Putar {{count}}",
|
||||||
"playlistWithCount_other": "{{count}} daftar putar",
|
"playlistWithCount_other": "{{count}} daftar putar",
|
||||||
"smartPlaylist": "$t(entity.playlist, {\"count\": 1}) pintar",
|
"smartPlaylist": "Cerdas $t(entity.playlist, {\"count\": 1})",
|
||||||
"track_other": "Pista",
|
"track_other": "Pista",
|
||||||
"song_other": "Lagu",
|
"song_other": "Lagu",
|
||||||
"trackWithCount_other": "{{count}} pista",
|
"trackWithCount_other": "{{count}} pista",
|
||||||
@@ -184,7 +193,7 @@
|
|||||||
"apiRouteError": "Tidak dapat mengarahkan permintaan",
|
"apiRouteError": "Tidak dapat mengarahkan permintaan",
|
||||||
"audioDeviceFetchError": "Terjadi kesalahan saat mencoba mengambil perangkat audio",
|
"audioDeviceFetchError": "Terjadi kesalahan saat mencoba mengambil perangkat audio",
|
||||||
"authenticationFailed": "Autentikasi gagal",
|
"authenticationFailed": "Autentikasi gagal",
|
||||||
"badAlbum": "Anda melihat halaman ini karena lagu ini tidak termasuk dalam album. Masalah ini bisa terjadi jika Anda memiliki lagu di tingkat atas folder musik Anda. Jellyfin hanya mengelompokkan lagu jika mereka berada di dalam folder",
|
"badAlbum": "Anda melihat halaman ini karena lagu ini bukan bagian dari album. Masalah ini kemungkinan besar muncul jika Anda memiliki lagu di tingkat teratas folder musik Anda. Jellyfin hanya mengelompokkan trek jika berada di dalam folder",
|
||||||
"credentialsRequired": "Kredensial diperlukan",
|
"credentialsRequired": "Kredensial diperlukan",
|
||||||
"endpointNotImplementedError": "Endpoint {{endpoint}} tidak diimplementasikan untuk {{serverType}}",
|
"endpointNotImplementedError": "Endpoint {{endpoint}} tidak diimplementasikan untuk {{serverType}}",
|
||||||
"genericError": "Terjadi kesalahan",
|
"genericError": "Terjadi kesalahan",
|
||||||
@@ -211,7 +220,8 @@
|
|||||||
"saveQueueFailed": "Gagal menyimpan antrean",
|
"saveQueueFailed": "Gagal menyimpan antrean",
|
||||||
"settingsSyncError": "Ditemukan ketidaksesuaian antara pengaturan di perender dan proses utama. mulai ulang aplikasi untuk menerapkan perubahan",
|
"settingsSyncError": "Ditemukan ketidaksesuaian antara pengaturan di perender dan proses utama. mulai ulang aplikasi untuk menerapkan perubahan",
|
||||||
"invalidJson": "JSON tidak valid",
|
"invalidJson": "JSON tidak valid",
|
||||||
"serverLockSingleServer": "Hanya satu server yang diizinkan ketika server dikunci"
|
"serverLockSingleServer": "Hanya satu server yang diizinkan ketika server dikunci",
|
||||||
|
"playbackPausedDueToError": "Pemutaran dijeda karena terjadi kesalahan"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"album": "$t(entity.album, {\"count\": 1})",
|
"album": "$t(entity.album, {\"count\": 1})",
|
||||||
@@ -286,7 +296,8 @@
|
|||||||
"success": "Ditambahkan $t(entity.trackWithCount, {\"count\": {{message}} }) ke $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
"success": "Ditambahkan $t(entity.trackWithCount, {\"count\": {{message}} }) ke $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||||
"title": "Tambahkan ke $t(entity.playlist, {\"count\": 1})",
|
"title": "Tambahkan ke $t(entity.playlist, {\"count\": 1})",
|
||||||
"create": "Buat $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
"create": "Buat $t(entity.playlist, {\"count\": 1}) {{playlist}}",
|
||||||
"searchOrCreate": "Cari $t(entity.playlist, {\"count\": 2}) atau ketik untuk membuat yang baru"
|
"searchOrCreate": "Cari $t(entity.playlist, {\"count\": 2}) atau ketik untuk membuat yang baru",
|
||||||
|
"noneAdded": "Tidak ada trek yang ditambahkan ke $t(entity.playlist, {\"count\": 1}) '{{playlist}}'"
|
||||||
},
|
},
|
||||||
"createPlaylist": {
|
"createPlaylist": {
|
||||||
"input_description": "$t(common.description)",
|
"input_description": "$t(common.description)",
|
||||||
@@ -321,12 +332,12 @@
|
|||||||
"clearFilters": "Hapus filter"
|
"clearFilters": "Hapus filter"
|
||||||
},
|
},
|
||||||
"shareItem": {
|
"shareItem": {
|
||||||
"allowDownloading": "Izinkan unduhan",
|
"allowDownloading": "Izinkan pengunduhan",
|
||||||
"description": "Deskripsi",
|
"description": "Deskripsi",
|
||||||
"setExpiration": "Atur masa berlaku",
|
"setExpiration": "Atur masa berlaku",
|
||||||
"success": "Tautan berbagi berhasil disalin ke papan klip (atau klik di sini untuk membuka)",
|
"success": "Tautan bagikan disalin ke papan klip (atau klik di sini untuk membukanya)",
|
||||||
"expireInvalid": "Masa berlaku harus di masa depan",
|
"expireInvalid": "Masa berlaku harus di waktu mendatang",
|
||||||
"createFailed": "Tidak dapat membuat sumber daya berbagi (Apakah berbagi diaktifkan?)",
|
"createFailed": "Gagal membuat bagikanan (apakah fitur berbagi diaktifkan?)",
|
||||||
"copyToClipboard": "Salin ke clipboard: Ctrl+C, enter",
|
"copyToClipboard": "Salin ke clipboard: Ctrl+C, enter",
|
||||||
"successMustClick": "Berbagi berhasil dibuat. klik di sini untuk membuka"
|
"successMustClick": "Berbagi berhasil dibuat. klik di sini untuk membuka"
|
||||||
},
|
},
|
||||||
@@ -368,19 +379,22 @@
|
|||||||
"enabled": "Mode pribadi diaktifkan, status pemutaran kini disembunyikan dari integrasi eksternal",
|
"enabled": "Mode pribadi diaktifkan, status pemutaran kini disembunyikan dari integrasi eksternal",
|
||||||
"disabled": "Mode pribadi dinonaktifkan, status pemutaran kini terlihat oleh integrasi eksternal yang diaktifkan",
|
"disabled": "Mode pribadi dinonaktifkan, status pemutaran kini terlihat oleh integrasi eksternal yang diaktifkan",
|
||||||
"title": "Mode pribadi"
|
"title": "Mode pribadi"
|
||||||
|
},
|
||||||
|
"editRadioStation": {
|
||||||
|
"success": "Stasiun radio berhasil diperbarui"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"albumArtistDetail": {
|
"albumArtistDetail": {
|
||||||
"about": "Tentang {{artist}}",
|
"about": "Tentang {{artist}}",
|
||||||
"recentReleases": "Rilis terbaru",
|
"recentReleases": "Rilisan terbaru",
|
||||||
"viewDiscography": "Lihat diskografi",
|
"viewDiscography": "Lihat diskografi",
|
||||||
"relatedArtists": "$t(entity.artist, {\"count\": 2}) serupa",
|
"relatedArtists": "$t(entity.artist, {\"count\": 2}) terkait",
|
||||||
"topSongs": "Lagu terbaik",
|
"topSongs": "Lagu teratas",
|
||||||
"topSongsFrom": "Lagu terbaik dari {{title}}",
|
"topSongsFrom": "Lagu teratas dari {{title}}",
|
||||||
"viewAll": "Lihat semua",
|
"viewAll": "Lihat semua",
|
||||||
"viewAllTracks": "Lihat semua $t(entity.track, {\"count\": 2})",
|
"viewAllTracks": "Lihat semua $t(entity.track, {\"count\": 2})",
|
||||||
"appearsOn": "Tampil di",
|
"appearsOn": "Muncul di",
|
||||||
"groupingTypeAll": "Semua jenis rilis",
|
"groupingTypeAll": "Semua jenis rilis",
|
||||||
"groupingTypePrimary": "Jenis rilis utama",
|
"groupingTypePrimary": "Jenis rilis utama",
|
||||||
"favoriteSongs": "Lagu favorit",
|
"favoriteSongs": "Lagu favorit",
|
||||||
@@ -447,7 +461,7 @@
|
|||||||
"setRating": "$t(action.setRating)",
|
"setRating": "$t(action.setRating)",
|
||||||
"playShuffled": "$t(player.shuffle)",
|
"playShuffled": "$t(player.shuffle)",
|
||||||
"shareItem": "Bagikan item",
|
"shareItem": "Bagikan item",
|
||||||
"showDetails": "Lihat detail",
|
"showDetails": "Dapatkan info",
|
||||||
"moveToTop": "$t(action.moveToTop)",
|
"moveToTop": "$t(action.moveToTop)",
|
||||||
"play": "$t(player.play)",
|
"play": "$t(player.play)",
|
||||||
"moveItems": "$t(action.moveItems)",
|
"moveItems": "$t(action.moveItems)",
|
||||||
@@ -470,7 +484,9 @@
|
|||||||
"unsynchronized": "Tidak sinkronisasi",
|
"unsynchronized": "Tidak sinkronisasi",
|
||||||
"useImageAspectRatio": "Gunakan rasio aspek gambar",
|
"useImageAspectRatio": "Gunakan rasio aspek gambar",
|
||||||
"lyricOffset": "Offset lirik (ms)",
|
"lyricOffset": "Offset lirik (ms)",
|
||||||
"lyricGap": "Jarak lirik"
|
"lyricGap": "Jarak lirik",
|
||||||
|
"lyricOpacityNonActive": "Opasitas lirik nonaktif",
|
||||||
|
"lyricScaleNonActive": "Skala lirik nonaktif"
|
||||||
},
|
},
|
||||||
"lyrics": "Lirik",
|
"lyrics": "Lirik",
|
||||||
"related": "Terkait",
|
"related": "Terkait",
|
||||||
@@ -503,7 +519,7 @@
|
|||||||
"itemDetail": {
|
"itemDetail": {
|
||||||
"copyPath": "Salin jalur ke papan klip",
|
"copyPath": "Salin jalur ke papan klip",
|
||||||
"copiedPath": "Jalur berhasil disalin",
|
"copiedPath": "Jalur berhasil disalin",
|
||||||
"openFile": "Tampilkan lagu di pengelola file"
|
"openFile": "Tampilkan trek di pengelola file"
|
||||||
},
|
},
|
||||||
"playlist": {
|
"playlist": {
|
||||||
"reorder": "Pengurutan ulang hanya diaktifkan saat mengurutkan berdasarkan ID"
|
"reorder": "Pengurutan ulang hanya diaktifkan saat mengurutkan berdasarkan ID"
|
||||||
@@ -631,19 +647,20 @@
|
|||||||
"sleepTimer_off": "Mati",
|
"sleepTimer_off": "Mati",
|
||||||
"sleepTimer_timeRemaining": "{{time}} tersisa",
|
"sleepTimer_timeRemaining": "{{time}} tersisa",
|
||||||
"sleepTimer_setCustom": "Atur pengatur waktu",
|
"sleepTimer_setCustom": "Atur pengatur waktu",
|
||||||
"sleepTimer_cancel": "Batalkan pengatur waktu"
|
"sleepTimer_cancel": "Batalkan pengatur waktu",
|
||||||
|
"scrobbleForceSubmit": "Paksa scrobble"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"accentColor": "Warna sorotan",
|
"accentColor": "Warna sorotan",
|
||||||
"accentColor_description": "Menetapkan warna sorotan aplikasi",
|
"accentColor_description": "Menetapkan warna sorotan aplikasi",
|
||||||
"albumBackground": "Gambar latar belakang album",
|
"albumBackground": "Gambar latar belakang album",
|
||||||
"albumBackground_description": "Tambahkan gambar latar belakang ke halaman album yang berisi sampul album",
|
"albumBackground_description": "Menambahkan gambar latar belakang untuk halaman album yang berisi sampul album",
|
||||||
"albumBackgroundBlur": "Ukuran blur gambar latar belakang album",
|
"albumBackgroundBlur": "Ukuran keburaman gambar latar belakang album",
|
||||||
"albumBackgroundBlur_description": "Atur tingkat blur gambar latar belakang album",
|
"albumBackgroundBlur_description": "Menyesuaikan tingkat keburaman yang diterapkan pada gambar latar belakang album",
|
||||||
"applicationHotkeys": "Tombol pintasan aplikasi",
|
"applicationHotkeys": "Tombol pintasan aplikasi",
|
||||||
"applicationHotkeys_description": "Menetapkan tombol pintasan aplikasi. centang untuk menjadikannya tombol pintasan global (desktop saja)",
|
"applicationHotkeys_description": "Menetapkan tombol pintasan aplikasi. centang untuk menjadikannya tombol pintasan global (desktop saja)",
|
||||||
"artistConfiguration": "Pengaturan halaman artis album",
|
"artistConfiguration": "Konfigurasi halaman artis album",
|
||||||
"artistConfiguration_description": "Atur elemen apa yang ditampilkan dan urutannya di halaman artis album",
|
"artistConfiguration_description": "Atur item apa saja yang ditampilkan, dan dalam urutan apa, pada halaman artis album",
|
||||||
"audioDevice": "Perangkat audio",
|
"audioDevice": "Perangkat audio",
|
||||||
"audioDevice_description": "Pilih perangkat audio yang digunakan untuk pemutaran",
|
"audioDevice_description": "Pilih perangkat audio yang digunakan untuk pemutaran",
|
||||||
"audioExclusiveMode": "Mode audio eksklusif",
|
"audioExclusiveMode": "Mode audio eksklusif",
|
||||||
@@ -657,12 +674,12 @@
|
|||||||
"windowBarStyle_description": "Pilih gaya bilah jendela",
|
"windowBarStyle_description": "Pilih gaya bilah jendela",
|
||||||
"zoom": "Persentase zoom",
|
"zoom": "Persentase zoom",
|
||||||
"zoom_description": "Tentukan persentase zoom aplikasi",
|
"zoom_description": "Tentukan persentase zoom aplikasi",
|
||||||
"clearCache_description": "'Pembersihan keras' Feishin. Untuk membersihkan cache Feishin, kosongkan cache browser (gambar yang disimpan dan elemen lainnya). Kredensial dan pengaturan server tetap terjaga",
|
"clearCache_description": "'Pembersihan keras' Feishin. Selain membersihkan cache Feishin, cache browser juga dikosongkan (gambar tersimpan dan aset lainnya). Kredensial server dan pengaturan tetap dipertahankan",
|
||||||
"clearQueryCache": "Bersihkan cache Feishin",
|
"clearQueryCache": "Bersihkan cache Feishin",
|
||||||
"clearQueryCache_description": "'Pembersihan lunak' Feishin. Ini akan menyegarkan daftar putar, metadata lagu, dan mengatur ulang lirik yang disimpan. Pengaturan, kredensial server, dan gambar cache tetap terjaga",
|
"clearQueryCache_description": "'Pembersihan lunak' Feishin. Ini akan menyegarkan daftar putar, metadata trek, dan mengatur ulang lirik yang disimpan. Pengaturan, kredensial server, dan gambar yang dicache tetap dipertahankan",
|
||||||
"clearCacheSuccess": "Cache berhasil dibersihkan",
|
"clearCacheSuccess": "Cache berhasil dikosongkan",
|
||||||
"contextMenu": "Pengaturan menu konteks (klik kanan)",
|
"contextMenu": "Konfigurasi menu konteks (klik kanan)",
|
||||||
"contextMenu_description": "Memungkinkan Anda menyembunyikan elemen yang ditampilkan dalam menu saat Anda klik kanan pada elemen. Elemen yang tidak dipilih akan disembunyikan",
|
"contextMenu_description": "Memungkinkan Anda menyembunyikan item yang ditampilkan di menu saat Anda mengeklik kanan suatu item. Item yang tidak dicentang akan disembunyikan",
|
||||||
"crossfadeDuration": "Durasi crossfade",
|
"crossfadeDuration": "Durasi crossfade",
|
||||||
"crossfadeDuration_description": "Atur durasi efek crossfade",
|
"crossfadeDuration_description": "Atur durasi efek crossfade",
|
||||||
"crossfadeStyle_description": "Pilih gaya crossfade yang digunakan oleh pemutar audio",
|
"crossfadeStyle_description": "Pilih gaya crossfade yang digunakan oleh pemutar audio",
|
||||||
@@ -677,7 +694,7 @@
|
|||||||
"discordApplicationId_description": "ID aplikasi untuk rich presence {{discord}} (default: {{defaultId}})",
|
"discordApplicationId_description": "ID aplikasi untuk rich presence {{discord}} (default: {{defaultId}})",
|
||||||
"discordIdleStatus": "Tampilkan status tidak aktif dalam status aktivitas",
|
"discordIdleStatus": "Tampilkan status tidak aktif dalam status aktivitas",
|
||||||
"discordIdleStatus_description": "Ketika diaktifkan, memperbarui status saat pemutar tidak aktif",
|
"discordIdleStatus_description": "Ketika diaktifkan, memperbarui status saat pemutar tidak aktif",
|
||||||
"discordListening": "Tampilkan status sebagai mendengarkan",
|
"discordListening": "Tampilkan status sebagai sedang mendengarkan",
|
||||||
"discordListening_description": "Tampilkan status sebagai mendengarkan alih-alih bermain",
|
"discordListening_description": "Tampilkan status sebagai mendengarkan alih-alih bermain",
|
||||||
"discordRichPresence_description": "Aktifkan status pemutaran di status aktivitas {{discord}}. Gambar tombol adalah: {{icon}}, {{playing}}, dan {{paused}}",
|
"discordRichPresence_description": "Aktifkan status pemutaran di status aktivitas {{discord}}. Gambar tombol adalah: {{icon}}, {{playing}}, dan {{paused}}",
|
||||||
"discordUpdateInterval": "Interval pembaruan status aktivitas {{discord}}",
|
"discordUpdateInterval": "Interval pembaruan status aktivitas {{discord}}",
|
||||||
@@ -685,7 +702,7 @@
|
|||||||
"enableRemote": "Aktifkan kontrol jarak jauh server",
|
"enableRemote": "Aktifkan kontrol jarak jauh server",
|
||||||
"enableRemote_description": "Aktifkan kontrol jarak jauh server untuk memungkinkan perangkat lain mengontrol aplikasi",
|
"enableRemote_description": "Aktifkan kontrol jarak jauh server untuk memungkinkan perangkat lain mengontrol aplikasi",
|
||||||
"externalLinks": "Tampilkan tautan eksternal",
|
"externalLinks": "Tampilkan tautan eksternal",
|
||||||
"externalLinks_description": "Izinkan untuk menampilkan tautan eksternal (Last.fm, MusicBrainz) di halaman artis/album",
|
"externalLinks_description": "Mengaktifkan penampilan tautan eksternal (Last.fm, MusicBrainz) pada halaman artis/album",
|
||||||
"exitToTray": "Keluar ke baki",
|
"exitToTray": "Keluar ke baki",
|
||||||
"exitToTray_description": "Keluar dari aplikasi ke baki sistem",
|
"exitToTray_description": "Keluar dari aplikasi ke baki sistem",
|
||||||
"followLyric": "Ikuti lirik saat ini",
|
"followLyric": "Ikuti lirik saat ini",
|
||||||
@@ -702,14 +719,14 @@
|
|||||||
"gaplessAudio_optionWeak": "Lemah (disarankan)",
|
"gaplessAudio_optionWeak": "Lemah (disarankan)",
|
||||||
"globalMediaHotkeys": "Tombol pintasan media global",
|
"globalMediaHotkeys": "Tombol pintasan media global",
|
||||||
"globalMediaHotkeys_description": "Aktifkan atau nonaktifkan penggunaan tombol pintasan sistem media untuk mengontrol pemutaran",
|
"globalMediaHotkeys_description": "Aktifkan atau nonaktifkan penggunaan tombol pintasan sistem media untuk mengontrol pemutaran",
|
||||||
"homeConfiguration": "Pengaturan halaman beranda",
|
"homeConfiguration": "Konfigurasi halaman beranda",
|
||||||
"homeConfiguration_description": "Mengatur elemen mana yang ditampilkan dan urutannya di halaman beranda",
|
"homeConfiguration_description": "Atur item apa saja yang ditampilkan, dan dalam urutan apa, pada halaman beranda",
|
||||||
"homeFeature": "Karusel fitur beranda",
|
"homeFeature": "Karusel unggulan beranda",
|
||||||
"homeFeature_description": "Mengontrol apakah karusel besar fitur ditampilkan di halaman beranda",
|
"homeFeature_description": "Mengatur apakah karusel unggulan besar ditampilkan di halaman beranda",
|
||||||
"hotkey_browserBack": "Mundur",
|
"hotkey_browserBack": "Mundur",
|
||||||
"hotkey_browserForward": "Maju",
|
"hotkey_browserForward": "Maju",
|
||||||
"hotkey_favoriteCurrentSong": "$t(common.currentSong) favorit",
|
"hotkey_favoriteCurrentSong": "Favoritkan $t(common.currentSong)",
|
||||||
"hotkey_favoritePreviousSong": "$t(common.previousSong) favorit",
|
"hotkey_favoritePreviousSong": "Favoritkan $t(common.previousSong)",
|
||||||
"hotkey_globalSearch": "Pencarian global",
|
"hotkey_globalSearch": "Pencarian global",
|
||||||
"hotkey_localSearch": "Pencarian di halaman",
|
"hotkey_localSearch": "Pencarian di halaman",
|
||||||
"hotkey_playbackNext": "Lagu berikutnya",
|
"hotkey_playbackNext": "Lagu berikutnya",
|
||||||
@@ -718,7 +735,7 @@
|
|||||||
"hotkey_playbackPlayPause": "Putar / jeda",
|
"hotkey_playbackPlayPause": "Putar / jeda",
|
||||||
"hotkey_playbackPrevious": "Lagu sebelumnya",
|
"hotkey_playbackPrevious": "Lagu sebelumnya",
|
||||||
"hotkey_playbackStop": "Berhenti",
|
"hotkey_playbackStop": "Berhenti",
|
||||||
"hotkey_rate0": "Bersihkan penilaian",
|
"hotkey_rate0": "Hapus penilaian",
|
||||||
"hotkey_rate1": "Beri penilaian 1 bintang",
|
"hotkey_rate1": "Beri penilaian 1 bintang",
|
||||||
"hotkey_rate2": "Beri penilaian 2 bintang",
|
"hotkey_rate2": "Beri penilaian 2 bintang",
|
||||||
"hotkey_rate3": "Beri penilaian 3 bintang",
|
"hotkey_rate3": "Beri penilaian 3 bintang",
|
||||||
@@ -732,15 +749,15 @@
|
|||||||
"hotkey_toggleQueue": "Ubah antrean",
|
"hotkey_toggleQueue": "Ubah antrean",
|
||||||
"hotkey_toggleRepeat": "Toggle ulangi",
|
"hotkey_toggleRepeat": "Toggle ulangi",
|
||||||
"hotkey_toggleShuffle": "Toggle acak",
|
"hotkey_toggleShuffle": "Toggle acak",
|
||||||
"hotkey_unfavoriteCurrentSong": "$t(common.currentSong) tidak favorit",
|
"hotkey_unfavoriteCurrentSong": "Batalkan favorit $t(common.currentSong)",
|
||||||
"hotkey_unfavoritePreviousSong": "$t(common.previousSong) tidak favorit",
|
"hotkey_unfavoritePreviousSong": "Batalkan favorit $t(common.previousSong)",
|
||||||
"hotkey_volumeDown": "Turunkan volume",
|
"hotkey_volumeDown": "Turunkan volume",
|
||||||
"hotkey_volumeMute": "Senyapkan volume",
|
"hotkey_volumeMute": "Senyapkan volume",
|
||||||
"hotkey_volumeUp": "Naikkan volume",
|
"hotkey_volumeUp": "Naikkan volume",
|
||||||
"hotkey_zoomIn": "Perbesar",
|
"hotkey_zoomIn": "Perbesar",
|
||||||
"hotkey_zoomOut": "Perkecil",
|
"hotkey_zoomOut": "Perkecil",
|
||||||
"imageAspectRatio": "Gunakan rasio aspek sampul asli",
|
"imageAspectRatio": "Gunakan rasio aspek asli sampul",
|
||||||
"imageAspectRatio_description": "Jika diaktifkan, sampul akan ditampilkan dengan rasio aspek aslinya. Untuk seni yang tidak 1:1, ruang yang tersisa akan kosong",
|
"imageAspectRatio_description": "Jika diaktifkan, sampul akan ditampilkan menggunakan rasio aspek aslinya. Untuk sampul yang tidak 1:1, ruang yang tersisa akan kosong",
|
||||||
"language_description": "Menetapkan bahasa untuk aplikasi ($t(common.restartRequired))",
|
"language_description": "Menetapkan bahasa untuk aplikasi ($t(common.restartRequired))",
|
||||||
"lastfmApiKey": "Kunci API untuk {{lastfm}}",
|
"lastfmApiKey": "Kunci API untuk {{lastfm}}",
|
||||||
"lastfmApiKey_description": "Kunci API untuk {{lastfm}}. Diperlukan untuk sampul",
|
"lastfmApiKey_description": "Kunci API untuk {{lastfm}}. Diperlukan untuk sampul",
|
||||||
@@ -769,8 +786,8 @@
|
|||||||
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||||
"playerbarOpenDrawer": "Buka pemutar ke layar penuh",
|
"playerbarOpenDrawer": "Tombol alih layar penuh bilah pemutar",
|
||||||
"playerbarOpenDrawer_description": "Izinkan mengklik bilah pemutar untuk membuka pemutar di layar penuh",
|
"playerbarOpenDrawer_description": "Memungkinkan bilah pemutar diklik untuk membuka pemutar layar penuh",
|
||||||
"remotePassword": "Kata sandi kontrol jarak jauh server",
|
"remotePassword": "Kata sandi kontrol jarak jauh server",
|
||||||
"remotePassword_description": "Tentukan kata sandi untuk kontrol jarak jauh server. Kredensial ini dikirimkan dengan tidak aman secara default, jadi Anda harus menggunakan kata sandi unik untuk menghindari masalah",
|
"remotePassword_description": "Tentukan kata sandi untuk kontrol jarak jauh server. Kredensial ini dikirimkan dengan tidak aman secara default, jadi Anda harus menggunakan kata sandi unik untuk menghindari masalah",
|
||||||
"remotePort": "Port kontrol jarak jauh server",
|
"remotePort": "Port kontrol jarak jauh server",
|
||||||
@@ -829,7 +846,7 @@
|
|||||||
"translationApiKey_description": "Kunci API untuk terjemahan (hanya endpoint layanan global)",
|
"translationApiKey_description": "Kunci API untuk terjemahan (hanya endpoint layanan global)",
|
||||||
"translationTargetLanguage": "Bahasa tujuan penerjemahan",
|
"translationTargetLanguage": "Bahasa tujuan penerjemahan",
|
||||||
"translationTargetLanguage_description": "Bahasa tujuan untuk penerjemahan",
|
"translationTargetLanguage_description": "Bahasa tujuan untuk penerjemahan",
|
||||||
"trayEnabled": "Tampilkan di area pemberitahuan",
|
"trayEnabled": "Tampilkan baki",
|
||||||
"trayEnabled_description": "Tampilkan/sembunyikan ikon/menu di area pemberitahuan. Jika dinonaktifkan, juga menonaktifkan meminimalkan/keluar ke baki",
|
"trayEnabled_description": "Tampilkan/sembunyikan ikon/menu di area pemberitahuan. Jika dinonaktifkan, juga menonaktifkan meminimalkan/keluar ke baki",
|
||||||
"useSystemTheme": "Gunakan tema sistem",
|
"useSystemTheme": "Gunakan tema sistem",
|
||||||
"useSystemTheme_description": "Ikuti preferensi terang atau gelap yang ditetapkan oleh sistem",
|
"useSystemTheme_description": "Ikuti preferensi terang atau gelap yang ditetapkan oleh sistem",
|
||||||
@@ -838,14 +855,13 @@
|
|||||||
"volumeWidth": "Lebar penggeser volume",
|
"volumeWidth": "Lebar penggeser volume",
|
||||||
"volumeWidth_description": "Lebar penggeser volume",
|
"volumeWidth_description": "Lebar penggeser volume",
|
||||||
"webAudio": "Gunakan audio web",
|
"webAudio": "Gunakan audio web",
|
||||||
"clearCache": "Bersihkan cache browser",
|
"clearCache": "Kosongkan cache browser",
|
||||||
"disableLibraryUpdateOnStartup": "Nonaktifkan pemeriksaan versi baru saat startup",
|
"disableLibraryUpdateOnStartup": "Nonaktifkan pemeriksaan versi baru saat startup",
|
||||||
"mpvExecutablePath": "Jalur executable mpv",
|
"mpvExecutablePath": "Jalur executable mpv",
|
||||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||||
"sampleRate": "Rasio sampel",
|
"sampleRate": "Rasio sampel",
|
||||||
"savePlayQueue": "Simpan antrean pemutaran",
|
"savePlayQueue": "Simpan antrean pemutaran",
|
||||||
"autoDJ": "DJ otomatis",
|
"autoDJ": "DJ Otomatis",
|
||||||
"autoDJ_description": "Tambahkan lagu serupa secara otomatis ke antrean",
|
|
||||||
"autoDJ_itemCount": "Jumlah item",
|
"autoDJ_itemCount": "Jumlah item",
|
||||||
"autoDJ_itemCount_description": "Jumlah item yang dicoba ditambahkan ke antrean saat DJ otomatis diaktifkan",
|
"autoDJ_itemCount_description": "Jumlah item yang dicoba ditambahkan ke antrean saat DJ otomatis diaktifkan",
|
||||||
"autoDJ_timing": "Waktu",
|
"autoDJ_timing": "Waktu",
|
||||||
@@ -990,36 +1006,76 @@
|
|||||||
"playerItemConfiguration": "Konfigurasi item pemutar",
|
"playerItemConfiguration": "Konfigurasi item pemutar",
|
||||||
"sidebarPlaylistListFilterRegex_description": "Sembunyikan playlist di bilah sisi yang cocok dengan ekspresi reguler ini",
|
"sidebarPlaylistListFilterRegex_description": "Sembunyikan playlist di bilah sisi yang cocok dengan ekspresi reguler ini",
|
||||||
"sidebarPlaylistListFilterRegex_placeholder": "Mis. ^daily mix.*",
|
"sidebarPlaylistListFilterRegex_placeholder": "Mis. ^daily mix.*",
|
||||||
"sidebarPlaylistListFilterRegex": "Regex filter playlist"
|
"sidebarPlaylistListFilterRegex": "Regex filter playlist",
|
||||||
|
"autosave": "Simpan antrean putar secara otomatis",
|
||||||
|
"autosave_description": "Aktifkan penyimpanan otomatis antrean putar ke server Anda. Ini hanya dimungkinkan saat menggunakan Navidrome/Subsonic, dan Anda tidak dapat memiliki antrean putar campuran.",
|
||||||
|
"autosaveCount": "Frekuensi penyimpanan otomatis antrean putar",
|
||||||
|
"autosaveCount_description": "Berapa banyak perubahan trek sebelum antrean disimpan. 1 (minimum) berarti setiap pergantian lagu",
|
||||||
|
"hotkey_listShowPlayingSong": "Tampilkan lagu yang sedang diputar dalam daftar",
|
||||||
|
"listenbrainz_description": "Tampilkan tautan ke ListenBrainz pada halaman artis/album",
|
||||||
|
"listenbrainz": "Tampilkan tautan ListenBrainz",
|
||||||
|
"qobuz_description": "Tampilkan tautan ke Qobuz pada halaman artis/album",
|
||||||
|
"qobuz": "Tampilkan tautan Qobuz",
|
||||||
|
"spotify_description": "Tampilkan tautan ke Spotify pada halaman artis/album",
|
||||||
|
"spotify": "Tampilkan tautan Spotify",
|
||||||
|
"nativeSpotify_description": "Buka di aplikasi Spotify alih-alih di browser Anda",
|
||||||
|
"nativeSpotify": "Gunakan aplikasi Spotify",
|
||||||
|
"playerbarWaveformStretch": "Peregangan bentuk gelombang",
|
||||||
|
"playerbarWaveformStretch_description": "Meregangkan bentuk gelombang untuk memenuhi ruang yang tersedia",
|
||||||
|
"preventSuspendOnPlayback_description": "Cegah aplikasi ditangguhkan saat musik diputar",
|
||||||
|
"preventSuspendOnPlayback": "Cegah penangguhan saat pemutaran",
|
||||||
|
"sidebarPlaylistFolders_description": "Buat tampilan folder untuk daftar putar yang menyertakan pemisah yang dikonfigurasi dalam namanya",
|
||||||
|
"sidebarPlaylistFolders": "Aktifkan folder",
|
||||||
|
"sidebarPlaylistFolderSeparator_description": "Karakter (atau string) yang memisahkan tingkat folder dalam nama daftar putar",
|
||||||
|
"sidebarPlaylistFolderSeparator": "Pemisah folder",
|
||||||
|
"sidebarPlaylistFolderView_description": "Cara folder ditampilkan di bilah sisi",
|
||||||
|
"sidebarPlaylistFolderView": "Tampilan folder",
|
||||||
|
"sidebarPlaylistFolderView_optionSingle": "Folder tunggal",
|
||||||
|
"sidebarPlaylistFolderView_optionTree": "Tampilan pohon",
|
||||||
|
"sidebarPlaylistFolderView_optionNavigation": "Tampilan navigasi",
|
||||||
|
"sidebarPlaylistFolderTreeIndent_description": "Jumlah piksel indentasi tiap tingkat pohon",
|
||||||
|
"sidebarPlaylistFolderTreeIndent": "Indentasi pohon",
|
||||||
|
"sidebarPlaylistFolderTreeLineColor_description": "Warna garis penghubung pohon (biarkan kosong untuk default tema)",
|
||||||
|
"sidebarPlaylistFolderTreeLineColor": "Warna garis pohon",
|
||||||
|
"sidebarPlaylistMode_description": "Cara setiap daftar putar ditampilkan dalam daftar bilah sisi",
|
||||||
|
"sidebarPlaylistMode": "Mode daftar putar bilah sisi",
|
||||||
|
"sidebarPlaylistMode_optionCompact": "Ringkas",
|
||||||
|
"sidebarPlaylistMode_optionExpanded": "Diperluas",
|
||||||
|
"sidePlayQueueLayout": "Tata letak antrean putar samping",
|
||||||
|
"sidePlayQueueLayout_description": "Mengatur tata letak antrean putar samping yang terlampir",
|
||||||
|
"sidePlayQueueLayout_optionHorizontal": "Horizontal",
|
||||||
|
"sidePlayQueueLayout_optionVertical": "Vertikal",
|
||||||
|
"waveformLoadingDelay": "Penundaan pemuatan bentuk gelombang",
|
||||||
|
"waveformLoadingDelay_description": "Penundaan dalam detik sebelum memuat bentuk gelombang. Tingkatkan nilai ini jika Anda mengalami tersendat saat menggunakan pemutar web."
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"column": {
|
"column": {
|
||||||
"album": "Album",
|
"album": "Album",
|
||||||
"albumArtist": "Artis album",
|
"albumArtist": "Artis album",
|
||||||
"albumCount": "$t(entity.album, {\"count\": 2})",
|
"albumCount": "Album",
|
||||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
"artist": "Artis",
|
||||||
"biography": "Biografi",
|
"biography": "Biografi",
|
||||||
"bitrate": "Bitrate",
|
"bitrate": "Bitrate",
|
||||||
"bpm": "Lpm",
|
"bpm": "Lpm",
|
||||||
"channels": "$t(common.channel, {\"count\": 2})",
|
"channels": "Saluran",
|
||||||
"codec": "$t(common.codec)",
|
"codec": "Kodek",
|
||||||
"comment": "Komentar",
|
"comment": "Komentar",
|
||||||
"dateAdded": "Tanggal ditambahkan",
|
"dateAdded": "Tanggal ditambahkan",
|
||||||
"discNumber": "Nomor disk",
|
"discNumber": "Nomor disk",
|
||||||
"favorite": "Favorit",
|
"favorite": "Favorit",
|
||||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
"genre": "Genre",
|
||||||
"lastPlayed": "Terakhir diputar",
|
"lastPlayed": "Terakhir diputar",
|
||||||
"path": "Jalur",
|
"path": "Jalur",
|
||||||
"playCount": "Putaran",
|
"playCount": "Putaran",
|
||||||
"rating": "Penilaian",
|
"rating": "Penilaian",
|
||||||
"releaseDate": "Tanggal rilis",
|
"releaseDate": "Tanggal rilis",
|
||||||
"releaseYear": "Tahun",
|
"releaseYear": "Tahun",
|
||||||
"size": "$t(common.size)",
|
"size": "Ukuran",
|
||||||
"songCount": "$t(entity.track, {\"count\": 2})",
|
"songCount": "Trek",
|
||||||
"title": "Judul",
|
"title": "Judul",
|
||||||
"trackNumber": "Pista",
|
"trackNumber": "Pista",
|
||||||
"bitDepth": "$t(common.bitDepth)",
|
"bitDepth": "Kedalaman Bit",
|
||||||
"sampleRate": "$t(common.sampleRate)",
|
"sampleRate": "Laju Sampel",
|
||||||
"owner": "Pemilik"
|
"owner": "Pemilik"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@@ -1307,6 +1363,13 @@
|
|||||||
"d": "D",
|
"d": "D",
|
||||||
"z": "Z"
|
"z": "Z"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"systemAudioConsentAllow": "Izinkan",
|
||||||
|
"systemAudioConsentBody": "Visualizer memerlukan akses ke audio sistem agar dapat berfungsi",
|
||||||
|
"systemAudioConsentDecline": "Tolak",
|
||||||
|
"systemAudioConsentTitle": "Izinkan akses ke audio sistem?",
|
||||||
|
"systemAudioCaptureFailed": "Tidak dapat memulai pengambilan: {{message}}",
|
||||||
|
"systemAudioNoAudioTrack": "Tidak ada trek audio yang dikembalikan. Pastikan pengambilan audio diaktifkan saat diminta.",
|
||||||
|
"systemAudioExclusiveModeNotSupported": "Visualizer tidak tersedia saat mode audio eksklusif diaktifkan. Nonaktifkan Mode Audio Eksklusif di pengaturan MPV lalu coba lagi."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -438,7 +438,6 @@
|
|||||||
"discordLinkType_none": "$t(common.none)",
|
"discordLinkType_none": "$t(common.none)",
|
||||||
"discordLinkType_mbz_lastfm": "{{musicbrainz}} con {{lastfm}} fallback",
|
"discordLinkType_mbz_lastfm": "{{musicbrainz}} con {{lastfm}} fallback",
|
||||||
"autoDJ": "Auto DJ",
|
"autoDJ": "Auto DJ",
|
||||||
"autoDJ_description": "Aggiungi automaticamente canzoni simili alla coda",
|
|
||||||
"autoDJ_itemCount": "Conteggio elementi",
|
"autoDJ_itemCount": "Conteggio elementi",
|
||||||
"analyticsDisable_description": "Alcuni dati anonimi sull'utilizzo vengono inviati allo sviluppatore per migliorare l'applicazione",
|
"analyticsDisable_description": "Alcuni dati anonimi sull'utilizzo vengono inviati allo sviluppatore per migliorare l'applicazione",
|
||||||
"artistBackground": "Immagine dello sfondo dell'artista",
|
"artistBackground": "Immagine dello sfondo dell'artista",
|
||||||
|
|||||||
@@ -315,7 +315,6 @@
|
|||||||
"exportImportSettings_importSuccess": "設定が正常にインポートされました!",
|
"exportImportSettings_importSuccess": "設定が正常にインポートされました!",
|
||||||
"exportImportSettings_importModalTitle": "Feishin 設定をインポート",
|
"exportImportSettings_importModalTitle": "Feishin 設定をインポート",
|
||||||
"exportImportSettings_importBtn": "設定をインポート",
|
"exportImportSettings_importBtn": "設定をインポート",
|
||||||
"autoDJ_description": "類似の曲を自動でキューに追加します",
|
|
||||||
"autoDJ": "自動 DJ",
|
"autoDJ": "自動 DJ",
|
||||||
"autoDJ_itemCount_description": "自動 DJ が有効なときにキューに追加しようとした曲数",
|
"autoDJ_itemCount_description": "自動 DJ が有効なときにキューに追加しようとした曲数",
|
||||||
"autoDJ_itemCount": "曲数",
|
"autoDJ_itemCount": "曲数",
|
||||||
|
|||||||
@@ -653,7 +653,6 @@
|
|||||||
"globalMediaHotkeys_description": "Het gebruik van systeem mediahotkeys voor controle van afspelen aan-/uitzetten",
|
"globalMediaHotkeys_description": "Het gebruik van systeem mediahotkeys voor controle van afspelen aan-/uitzetten",
|
||||||
"globalMediaHotkeys": "Globale mediasneltoetsen",
|
"globalMediaHotkeys": "Globale mediasneltoetsen",
|
||||||
"autoDJ": "Auto-DJ",
|
"autoDJ": "Auto-DJ",
|
||||||
"autoDJ_description": "Soortgelijke nummers automatisch aan wachtrij toevoegen",
|
|
||||||
"autoDJ_itemCount": "Aantal items",
|
"autoDJ_itemCount": "Aantal items",
|
||||||
"autoDJ_itemCount_description": "Het aantal items dat aan de wachtrij wordt geprobeerd toe te voegen als auto-DJ is ingeschakeld",
|
"autoDJ_itemCount_description": "Het aantal items dat aan de wachtrij wordt geprobeerd toe te voegen als auto-DJ is ingeschakeld",
|
||||||
"autoDJ_timing": "Timing",
|
"autoDJ_timing": "Timing",
|
||||||
|
|||||||
@@ -173,7 +173,8 @@
|
|||||||
"newVersionAvailable": "Nowa wersja jest dostępna",
|
"newVersionAvailable": "Nowa wersja jest dostępna",
|
||||||
"numberOfResults": "{{numberOfResults}} wyników",
|
"numberOfResults": "{{numberOfResults}} wyników",
|
||||||
"grouping": "Grupowanie",
|
"grouping": "Grupowanie",
|
||||||
"back": "Wstecz"
|
"back": "Wstecz",
|
||||||
|
"openFolder": "Otwórz folder"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"genre_one": "Gatunek",
|
"genre_one": "Gatunek",
|
||||||
@@ -409,7 +410,12 @@
|
|||||||
"input_played": "Filtr odtwarzania",
|
"input_played": "Filtr odtwarzania",
|
||||||
"input_played_optionAll": "Wszystkie utwory",
|
"input_played_optionAll": "Wszystkie utwory",
|
||||||
"input_played_optionUnplayed": "Tylko nieodtworzone utwory",
|
"input_played_optionUnplayed": "Tylko nieodtworzone utwory",
|
||||||
"input_played_optionPlayed": "Tylko odtworzone utwory"
|
"input_played_optionPlayed": "Tylko odtworzone utwory",
|
||||||
|
"input_kind_albums": "Albumy",
|
||||||
|
"input_kind_songs": "Piosenki",
|
||||||
|
"input_kind": "Losowy wybór",
|
||||||
|
"input_limit_albums": "Ile albumów?",
|
||||||
|
"input_limit_songs": "Ile piosenek?"
|
||||||
},
|
},
|
||||||
"saveQueue": {
|
"saveQueue": {
|
||||||
"success": "Zapisano kolejkę odtwarzania na serwerze"
|
"success": "Zapisano kolejkę odtwarzania na serwerze"
|
||||||
@@ -883,7 +889,7 @@
|
|||||||
"customCssEnable": "Włącz niestandardowy CSS",
|
"customCssEnable": "Włącz niestandardowy CSS",
|
||||||
"customCssEnable_description": "Pozwalaj na pisanie niestandardowego CSS",
|
"customCssEnable_description": "Pozwalaj na pisanie niestandardowego CSS",
|
||||||
"customCssNotice": "Ostrzeżenie: chociaż istnieje pewne filtrowanie (uniemożliwia używanie URL() i content:), używanie niestandardowego CSS-a może stwarzać ryzyko przez zmiany w interfejsie",
|
"customCssNotice": "Ostrzeżenie: chociaż istnieje pewne filtrowanie (uniemożliwia używanie URL() i content:), używanie niestandardowego CSS-a może stwarzać ryzyko przez zmiany w interfejsie",
|
||||||
"customCss_description": "Zawartość niestandardowego CSS. Uwaga: content i zdalne URL są niedozwolonymi właściwościami. Podgląd twojej zawartości jest pokazana poniżej. Dodatkowe pola których nie ustawiłeś, są obecne z powodu sanityzacji",
|
"customCss_description": "Zawartość niestandardowego CSS. Uwaga: content i zdalne URL są niedozwolonymi właściwościami. Podgląd twojej zawartości jest pokazany poniżej. Dodatkowe pola których nie ustawiłeś są obecne z powodu sanityzacji. Aplikacja komputerowa: feishin odczytuje i zapisuje custom.css w katalogu ustawień aplikacji i przeładowuje go gdy plik się zmieni",
|
||||||
"customCss": "Niestandardowy css",
|
"customCss": "Niestandardowy css",
|
||||||
"trayEnabled_description": "Pokaż/ukryj ikonę/menu w zasobniku. jeżeli wyłączone, wyłącza też minimalizowanie.wyjście do zasobnika",
|
"trayEnabled_description": "Pokaż/ukryj ikonę/menu w zasobniku. jeżeli wyłączone, wyłącza też minimalizowanie.wyjście do zasobnika",
|
||||||
"webAudio_description": "Używaj web audio. Włącza to zaawansowane funkcje takie jak ReplayGain. Wyłącz jeżeli nie działa poprawnie",
|
"webAudio_description": "Używaj web audio. Włącza to zaawansowane funkcje takie jak ReplayGain. Wyłącz jeżeli nie działa poprawnie",
|
||||||
@@ -989,9 +995,8 @@
|
|||||||
"audioFadeOnStatusChange": "Przenikanie dźwięku przy zmianie statusu",
|
"audioFadeOnStatusChange": "Przenikanie dźwięku przy zmianie statusu",
|
||||||
"audioFadeOnStatusChange_description": "Umożliwia zanikanie lub pojawianie się dźwięku gdy zmieni się status play/pauza",
|
"audioFadeOnStatusChange_description": "Umożliwia zanikanie lub pojawianie się dźwięku gdy zmieni się status play/pauza",
|
||||||
"autoDJ": "Automatyczny DJ",
|
"autoDJ": "Automatyczny DJ",
|
||||||
"autoDJ_description": "Automatycznie dodawaj podobne piosenki do kolejki",
|
|
||||||
"autoDJ_itemCount": "Liczba elementów",
|
"autoDJ_itemCount": "Liczba elementów",
|
||||||
"autoDJ_itemCount_description": "Liczba elementów, które będzie próbować dodać do kolejki kiedy automatyczny DJ jest włączony",
|
"autoDJ_itemCount_description": "Liczba elementów, które będzie próbować dodać do kolejki",
|
||||||
"autoDJ_timing": "Czas dodawania",
|
"autoDJ_timing": "Czas dodawania",
|
||||||
"autoDJ_timing_description": "Ilość piosenek pozostałych w kolejce przed tym gdy zostanie włączony automatyczny DJ",
|
"autoDJ_timing_description": "Ilość piosenek pozostałych w kolejce przed tym gdy zostanie włączony automatyczny DJ",
|
||||||
"logLevel": "Poziom logów",
|
"logLevel": "Poziom logów",
|
||||||
@@ -1093,7 +1098,16 @@
|
|||||||
"sidebarPlaylistMode_description": "Jak każda z playlist jest wyświetlana w liście w pasku bocznym",
|
"sidebarPlaylistMode_description": "Jak każda z playlist jest wyświetlana w liście w pasku bocznym",
|
||||||
"sidebarPlaylistMode": "Tryb playlist bocznego paska",
|
"sidebarPlaylistMode": "Tryb playlist bocznego paska",
|
||||||
"sidebarPlaylistMode_optionCompact": "Kompaktowy",
|
"sidebarPlaylistMode_optionCompact": "Kompaktowy",
|
||||||
"sidebarPlaylistMode_optionExpanded": "Rozszerzony"
|
"sidebarPlaylistMode_optionExpanded": "Rozszerzony",
|
||||||
|
"autoDJ_mode": "Tryb",
|
||||||
|
"autoDJ_mode_albums": "Albumy",
|
||||||
|
"autoDJ_mode_description": "Wybierz dodawanie piosenek lub całych albumów do kolejki",
|
||||||
|
"autoDJ_mode_songs": "Piosenki",
|
||||||
|
"autoDJ_enabled": "Włącz Auto DJ",
|
||||||
|
"autoDJ_albumStrategy": "Tryb wyboru albumów",
|
||||||
|
"autoDJ_songStrategy": "Tryb wyboru piosenek",
|
||||||
|
"autoDJ_strategy_option_library_random": "Losowo",
|
||||||
|
"autoDJ_strategy_option_similar": "Podobne"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
|
|||||||
+348
-17
@@ -20,8 +20,25 @@
|
|||||||
"viewPlaylists": "Ver $t(entity.playlist, {\"count\": 2})",
|
"viewPlaylists": "Ver $t(entity.playlist, {\"count\": 2})",
|
||||||
"openIn": {
|
"openIn": {
|
||||||
"lastfm": "Abrir em Last.fm",
|
"lastfm": "Abrir em Last.fm",
|
||||||
"musicbrainz": "Abrir em MusicBrainz"
|
"musicbrainz": "Abrir em MusicBrainz",
|
||||||
}
|
"listenbrainz": "Abrir no ListenBrainz",
|
||||||
|
"qobuz": "Abrir no Qobuz",
|
||||||
|
"spotify": "Abrir no Spotify"
|
||||||
|
},
|
||||||
|
"addOrRemoveFromSelection": "Adicionar ou remover da seleção",
|
||||||
|
"goToCurrent": "Ir para o elemento atual",
|
||||||
|
"collapseAllFolders": "Colapsar todas as pastas",
|
||||||
|
"expandAllFolders": "Expandir todas as pastas",
|
||||||
|
"createRadioStation": "Criar $t(entity.radioStation, {\"count\": 1})",
|
||||||
|
"deleteRadioStation": "Apagar $t(entity.radioStation, {\"count\": 1})",
|
||||||
|
"selectAll": "Selecionar tudo",
|
||||||
|
"moveUp": "Mover para cima",
|
||||||
|
"moveDown": "Mover para baixo",
|
||||||
|
"holdToMoveToTop": "Segure para ir ao topo",
|
||||||
|
"holdToMoveToBottom": "Mover para ir ao fundo",
|
||||||
|
"moveItems": "Mover elementos",
|
||||||
|
"viewMore": "Ver mais",
|
||||||
|
"openApplicationDirectory": "Abrir a pasta da aplicação"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"action_one": "Ação",
|
"action_one": "Ação",
|
||||||
@@ -122,7 +139,21 @@
|
|||||||
"unknown": "Desconhecido",
|
"unknown": "Desconhecido",
|
||||||
"version": "Versão",
|
"version": "Versão",
|
||||||
"year": "Ano",
|
"year": "Ano",
|
||||||
"yes": "Sim"
|
"yes": "Sim",
|
||||||
|
"countSelected": "{{count}} selecionado",
|
||||||
|
"bitDepth": "Profundidade de bits",
|
||||||
|
"example": "Exemplo",
|
||||||
|
"externalLinks": "Ligações externas",
|
||||||
|
"mood": "Humor",
|
||||||
|
"private": "Privado",
|
||||||
|
"public": "Público",
|
||||||
|
"retry": "Tentar novamente",
|
||||||
|
"rename": "Renomear",
|
||||||
|
"sampleRate": "Taxa de amostragem",
|
||||||
|
"sort": "Ordenar",
|
||||||
|
"clean": "Limpar",
|
||||||
|
"itemsMore": "{{count}} mais",
|
||||||
|
"newVersionAvailable": "Uma nova versão está disponível"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"album_one": "Álbum",
|
"album_one": "Álbum",
|
||||||
@@ -201,7 +232,9 @@
|
|||||||
"serverNotSelectedError": "Nenhum servidor selecionado",
|
"serverNotSelectedError": "Nenhum servidor selecionado",
|
||||||
"serverRequired": "Servidor necessário",
|
"serverRequired": "Servidor necessário",
|
||||||
"sessionExpiredError": "A sua sessão expirou",
|
"sessionExpiredError": "A sua sessão expirou",
|
||||||
"systemFontError": "Ocorreu um erro ao tentar obter fontes do sistema"
|
"systemFontError": "Ocorreu um erro ao tentar obter fontes do sistema",
|
||||||
|
"invalidJson": "JSON inválido",
|
||||||
|
"noNetwork": "Servidor não disponível"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"album": "$t(entity.album, {\"count\": 1})",
|
"album": "$t(entity.album, {\"count\": 1})",
|
||||||
@@ -245,7 +278,10 @@
|
|||||||
"songCount": "Contador de músicas",
|
"songCount": "Contador de músicas",
|
||||||
"title": "Titulo",
|
"title": "Titulo",
|
||||||
"toYear": "Até o ano",
|
"toYear": "Até o ano",
|
||||||
"trackNumber": "Faixa"
|
"trackNumber": "Faixa",
|
||||||
|
"matchAnd": "E",
|
||||||
|
"matchOr": "Ou",
|
||||||
|
"sortName": "Ordenar por nome"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"addServer": {
|
"addServer": {
|
||||||
@@ -259,7 +295,8 @@
|
|||||||
"input_url": "Url",
|
"input_url": "Url",
|
||||||
"input_username": "Nome de utilizador",
|
"input_username": "Nome de utilizador",
|
||||||
"success": "Servidor adicionado com sucesso",
|
"success": "Servidor adicionado com sucesso",
|
||||||
"title": "Adicionar servidor"
|
"title": "Adicionar servidor",
|
||||||
|
"input_remoteUrl": "URL público"
|
||||||
},
|
},
|
||||||
"addToPlaylist": {
|
"addToPlaylist": {
|
||||||
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
|
||||||
@@ -292,7 +329,9 @@
|
|||||||
},
|
},
|
||||||
"queryEditor": {
|
"queryEditor": {
|
||||||
"input_optionMatchAll": "Corresponder todos",
|
"input_optionMatchAll": "Corresponder todos",
|
||||||
"input_optionMatchAny": "Corresponder qualquer um"
|
"input_optionMatchAny": "Corresponder qualquer um",
|
||||||
|
"resetToDefault": "Restaurar à predefinição",
|
||||||
|
"clearFilters": "Limpar filtros"
|
||||||
},
|
},
|
||||||
"shareItem": {
|
"shareItem": {
|
||||||
"allowDownloading": "Permitir descargas",
|
"allowDownloading": "Permitir descargas",
|
||||||
@@ -305,6 +344,21 @@
|
|||||||
"updateServer": {
|
"updateServer": {
|
||||||
"success": "Servidor atualizado com sucesso",
|
"success": "Servidor atualizado com sucesso",
|
||||||
"title": "Atualizar servidor"
|
"title": "Atualizar servidor"
|
||||||
|
},
|
||||||
|
"createRadioStation": {
|
||||||
|
"title": "Criar estação de rádio",
|
||||||
|
"input_name": "Nome"
|
||||||
|
},
|
||||||
|
"lyricsExport": {
|
||||||
|
"input_synced": "Exportar letras sincronizadas"
|
||||||
|
},
|
||||||
|
"shuffleAll": {
|
||||||
|
"title": "Tocar aleatório",
|
||||||
|
"input_minYear": "A partir do ano",
|
||||||
|
"input_maxYear": "Até o ano"
|
||||||
|
},
|
||||||
|
"privateMode": {
|
||||||
|
"title": "Modo Privado"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
@@ -317,7 +371,9 @@
|
|||||||
"topSongs": "Músicas mais tocadas",
|
"topSongs": "Músicas mais tocadas",
|
||||||
"topSongsFrom": "Músicas mais tocadas de {{title}}",
|
"topSongsFrom": "Músicas mais tocadas de {{title}}",
|
||||||
"viewAll": "Ver tudo",
|
"viewAll": "Ver tudo",
|
||||||
"viewAllTracks": "Ver todas as $t(entity.track, {\"count\": 2})"
|
"viewAllTracks": "Ver todas as $t(entity.track, {\"count\": 2})",
|
||||||
|
"topSongsCommunity": "Comunidade",
|
||||||
|
"topSongsPersonal": "Pessoal"
|
||||||
},
|
},
|
||||||
"albumArtistList": {
|
"albumArtistList": {
|
||||||
"title": "$t(entity.albumArtist, {\"count\": 2})"
|
"title": "$t(entity.albumArtist, {\"count\": 2})"
|
||||||
@@ -374,7 +430,8 @@
|
|||||||
"setRating": "$t(action.setRating)",
|
"setRating": "$t(action.setRating)",
|
||||||
"playShuffled": "$t(player.shuffle)",
|
"playShuffled": "$t(player.shuffle)",
|
||||||
"shareItem": "Partilhar elemento",
|
"shareItem": "Partilhar elemento",
|
||||||
"showDetails": "Obter informações"
|
"showDetails": "Obter informações",
|
||||||
|
"goTo": "Ir para"
|
||||||
},
|
},
|
||||||
"fullscreenPlayer": {
|
"fullscreenPlayer": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -417,7 +474,8 @@
|
|||||||
"mostPlayed": "Mais tocado",
|
"mostPlayed": "Mais tocado",
|
||||||
"newlyAdded": "Lançamentos recém-adicionados",
|
"newlyAdded": "Lançamentos recém-adicionados",
|
||||||
"recentlyPlayed": "Tocado recentemente",
|
"recentlyPlayed": "Tocado recentemente",
|
||||||
"title": "$t(common.home)"
|
"title": "$t(common.home)",
|
||||||
|
"genres": "$t(entity.genre, {\"count\": 2})"
|
||||||
},
|
},
|
||||||
"itemDetail": {
|
"itemDetail": {
|
||||||
"copyPath": "Copiar caminho para a área de transferência",
|
"copyPath": "Copiar caminho para a área de transferência",
|
||||||
@@ -435,7 +493,18 @@
|
|||||||
"generalTab": "Geral",
|
"generalTab": "Geral",
|
||||||
"hotkeysTab": "Teclas de atalho",
|
"hotkeysTab": "Teclas de atalho",
|
||||||
"playbackTab": "Reprodução",
|
"playbackTab": "Reprodução",
|
||||||
"windowTab": "Janela"
|
"windowTab": "Janela",
|
||||||
|
"application": "Aplicação",
|
||||||
|
"queryBuilder": "Construtor de Consultas",
|
||||||
|
"theme": "Tema",
|
||||||
|
"controls": "Controles",
|
||||||
|
"sidebar": "Barra lateral",
|
||||||
|
"remote": "Remoto",
|
||||||
|
"exportImport": "Importar/exportar",
|
||||||
|
"audio": "Áudio",
|
||||||
|
"lyrics": "Letras",
|
||||||
|
"transcoding": "Transcodificar",
|
||||||
|
"discord": "Discord"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"albumArtists": "$t(entity.albumArtist, {\"count\": 2})",
|
"albumArtists": "$t(entity.albumArtist, {\"count\": 2})",
|
||||||
@@ -450,12 +519,19 @@
|
|||||||
"search": "$t(common.search)",
|
"search": "$t(common.search)",
|
||||||
"settings": "$t(common.setting, {\"count\": 2})",
|
"settings": "$t(common.setting, {\"count\": 2})",
|
||||||
"shared": "$t(entity.playlist, {\"count\": 2}) partilhada",
|
"shared": "$t(entity.playlist, {\"count\": 2}) partilhada",
|
||||||
"tracks": "$t(entity.track, {\"count\": 2})"
|
"tracks": "$t(entity.track, {\"count\": 2})",
|
||||||
|
"collections": "Coleções"
|
||||||
},
|
},
|
||||||
"trackList": {
|
"trackList": {
|
||||||
"artistTracks": "Faixas de {{artist}}",
|
"artistTracks": "Faixas de {{artist}}",
|
||||||
"genreTracks": "\"{{genre}}\" $t(entity.track, {\"count\": 2})",
|
"genreTracks": "\"{{genre}}\" $t(entity.track, {\"count\": 2})",
|
||||||
"title": "$t(entity.track, {\"count\": 2})"
|
"title": "$t(entity.track, {\"count\": 2})"
|
||||||
|
},
|
||||||
|
"radioList": {
|
||||||
|
"title": "Estações de rádio"
|
||||||
|
},
|
||||||
|
"folderList": {
|
||||||
|
"title": "$t(entity.folder, {\"count\": 2})"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
@@ -489,7 +565,11 @@
|
|||||||
"toggleFullscreenPlayer": "Alternar player de ecrã cheio",
|
"toggleFullscreenPlayer": "Alternar player de ecrã cheio",
|
||||||
"unfavorite": "Remover favorito",
|
"unfavorite": "Remover favorito",
|
||||||
"pause": "Pausar",
|
"pause": "Pausar",
|
||||||
"viewQueue": "Ver fila"
|
"viewQueue": "Ver fila",
|
||||||
|
"lyrics": "Letra",
|
||||||
|
"sleepTimer_minutes": "{{count}} min",
|
||||||
|
"sleepTimer_hours": "{{count}} hr",
|
||||||
|
"sleepTimer_off": "Desligado"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"accentColor": "Cor de realce",
|
"accentColor": "Cor de realce",
|
||||||
@@ -528,18 +608,269 @@
|
|||||||
"discordApplicationId": "{{discord}} ID da aplicação",
|
"discordApplicationId": "{{discord}} ID da aplicação",
|
||||||
"discordIdleStatus_description": "Quando ativado, atualiza o estado enquanto o player está ocioso",
|
"discordIdleStatus_description": "Quando ativado, atualiza o estado enquanto o player está ocioso",
|
||||||
"discordUpdateInterval_description": "O tempo em segundos entre cada atualização (mínimo 15 segundos)",
|
"discordUpdateInterval_description": "O tempo em segundos entre cada atualização (mínimo 15 segundos)",
|
||||||
"playButtonBehavior_description": "Define o comportamento padrão do botão play ao adicionar músicas à fila"
|
"playButtonBehavior_description": "Define o comportamento padrão do botão play ao adicionar músicas à fila",
|
||||||
|
"autoDJ_itemCount": "Número de elementos",
|
||||||
|
"autoDJ_timing": "Tempo",
|
||||||
|
"customCss_description": "Conteúdo CSS personalizado. Observação: conteúdo e urls remotas são propriedades não permitidas. Uma pré-visualização do seu conteúdo é exibida abaixo. Campos adicionais que não definiu estão presentes devido à sanitização",
|
||||||
|
"automaticUpdates": "Atualizações automáticas",
|
||||||
|
"releaseChannel_optionBeta": "Beta",
|
||||||
|
"releaseChannel_optionLatest": "Mais recente",
|
||||||
|
"discordApplicationId_description": "O ID da aplicação para o rich presence do {{discord}} (defaults: {{defaultId}})",
|
||||||
|
"discordDisplayType_artistname": "Nome(s) do(s) artista(s)",
|
||||||
|
"discordDisplayType": "Tipo de exibição da presença do {{discord}}",
|
||||||
|
"discordIdleStatus": "Mostrar estado ocioso do rich presence",
|
||||||
|
"discordLinkType_mbz_lastfm": "{{musicbrainz}} com alternativa para {{lastfm}}",
|
||||||
|
"discordLinkType_none": "$t(common.none)",
|
||||||
|
"discordLinkType": "Ligações de presença do {{discord}}",
|
||||||
|
"discordServeImage_description": "Partilhar a capa para o rich presence do {{discord}} a partir do próprio servidor, disponível apenas para Jellyfin e Navidrome. O {{discord}} usa um bot para buscar imagens, portanto o seu servidor deve estar acessível pela internet pública",
|
||||||
|
"discordUpdateInterval": "Intervalo de atualização do rich presence do {{discord}}",
|
||||||
|
"exportImportSettings_control_importText": "Importar configurações",
|
||||||
|
"exportImportSettings_control_title": "Importar / exportar configurações",
|
||||||
|
"exportImportSettings_importBtn": "Importar configurações",
|
||||||
|
"exportImportSettings_importModalTitle": "Importar configurações do Feishin",
|
||||||
|
"externalLinks": "Mostrar ligações externas",
|
||||||
|
"font": "Fonte",
|
||||||
|
"fontType_optionBuiltIn": "Fonte embutida",
|
||||||
|
"fontType_optionCustom": "Fonte personalizada",
|
||||||
|
"fontType_optionSystem": "Fonte do sistema",
|
||||||
|
"fontType": "Tipo da fonte",
|
||||||
|
"homeFeatureStyle_optionMultiple": "Múltiplos",
|
||||||
|
"hotkey_globalSearch": "Pequisa global",
|
||||||
|
"hotkey_playbackPause": "Pausar",
|
||||||
|
"hotkey_playbackPlay": "Tocar",
|
||||||
|
"hotkey_playbackPlayPause": "Play / pausar",
|
||||||
|
"hotkey_playbackStop": "Parar",
|
||||||
|
"hotkey_volumeMute": "Volume mudo",
|
||||||
|
"hotkey_volumeUp": "Aumentar volume",
|
||||||
|
"hotkey_zoomIn": "Aproximar",
|
||||||
|
"hotkey_zoomOut": "Afastar",
|
||||||
|
"imageAspectRatio_description": "Se ativado, a capa será exibida usando a sua proporção nativa. Para capas que não forem 1:1, o espaço restante ficará vazio",
|
||||||
|
"imageAspectRatio": "Usar proporção nativa da capa",
|
||||||
|
"language_description": "Define o idioma da aplicação ($t(common.restartRequired))",
|
||||||
|
"lastfm_description": "Exibir ligações para o Last.fm nas páginas de artista/álbum",
|
||||||
|
"lastfm": "Mostrar ligações do Last.fm",
|
||||||
|
"lastfmApiKey_description": "A chave de API para {{lastfm}}. Necessária para capas de álbuns",
|
||||||
|
"lastfmApiKey": "{{lastfm}} chave API",
|
||||||
|
"lyricFetch_description": "Buscar letras em várias fontes da internet",
|
||||||
|
"lyricFetch": "Buscar letras na internet",
|
||||||
|
"lyricFetchProvider": "Provedores para buscar letras",
|
||||||
|
"lyricOffset_description": "Compensar a letra pelo valor especificado em milissegundos",
|
||||||
|
"lyricOffset": "Compensação da letra (ms)",
|
||||||
|
"minimizeToTray_description": "Minimizar a aplicação para a bandeja do sistema",
|
||||||
|
"minimizeToTray": "Minimizar para a bandeja",
|
||||||
|
"minimumScrobblePercentage_description": "O percentual mínimo da música que deve ser reproduzido antes de ser scrobblada",
|
||||||
|
"minimumScrobblePercentage": "Duração mínima para scrobble (percentual)",
|
||||||
|
"minimumScrobbleSeconds_description": "A duração mínima em segundos da música que deve ser reproduzida antes de ser scrobblada",
|
||||||
|
"minimumScrobbleSeconds": "Scrobble mínimo (segundos)",
|
||||||
|
"mpvExecutablePath": "Caminho do executável do mpv",
|
||||||
|
"mpvExtraParameters_help": "Um por linha",
|
||||||
|
"musicbrainz_description": "Exibir ligações para o MusicBrainz nas páginas de artista/álbum, quando o ID do MusicBrainz existir",
|
||||||
|
"musicbrainz": "Mostrar ligações do MusicBrainz",
|
||||||
|
"neteaseTranslation_description": "Quando ativado, busca e exibe letras traduzidas do NetEase, se disponíveis",
|
||||||
|
"neteaseTranslation": "Ativar traduções do NetEase",
|
||||||
|
"passwordStore_description": "Qual armazenamento de palavras-passe/segredos usar. Altere isto se tem problemas para armazenar palavras-passe",
|
||||||
|
"playbackStyle_description": "Selecione o estilo de reprodução a ser usado pelo reprodutor de áudio",
|
||||||
|
"playbackStyle_optionCrossFade": "Transição suave",
|
||||||
|
"playbackStyle_optionNormal": "Normal",
|
||||||
|
"playbackStyle": "Estilo de reprodução",
|
||||||
|
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||||
|
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||||
|
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||||
|
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||||
|
"playButtonBehavior": "Comportamento do botão de reprodução",
|
||||||
|
"imageResolution_optionSidebar": "Barra lateral",
|
||||||
|
"imageResolution_optionHeader": "Cabeçalho",
|
||||||
|
"imageResolution_optionFullScreenPlayer": "Reprodutor de ecrã cheio",
|
||||||
|
"playerbarOpenDrawer_description": "Permite clicar na barra do reprodutor para abrir o reprodutor em ecrã cheio",
|
||||||
|
"playerbarOpenDrawer": "Alternar ecrã cheio na barra do reprodutor",
|
||||||
|
"playerbarSliderType_optionWaveform": "Forma de onda",
|
||||||
|
"playerbarWaveformAlign_optionTop": "Topo",
|
||||||
|
"playerbarWaveformAlign_optionCenter": "Centro",
|
||||||
|
"playerbarWaveformAlign_optionBottom": "Fundo",
|
||||||
|
"showRatings_description": "Exibir ou ocultar as avaliações por estrelas",
|
||||||
|
"showRatings": "Exibir avaliações por estrelas",
|
||||||
|
"remotePassword_description": "Define a palavra-passe do servidor de controlo remoto. Estas credenciais, por padrão, são transferidas de forma insegura — use uma palavra-passe única da qualnão dependa",
|
||||||
|
"remotePort": "Porta do servidor de controlo remoto",
|
||||||
|
"replayGainClipping_description": "Evitar clipping causado pelo {{ReplayGain}} reduzindo automaticamente o ganho",
|
||||||
|
"replayGainClipping": "Clipping do {{ReplayGain}}",
|
||||||
|
"replayGainFallback": "Fallback do {{ReplayGain}}",
|
||||||
|
"replayGainMode_optionAlbum": "$t(entity.album, {\"count\": 1})",
|
||||||
|
"replayGainMode_optionNone": "$t(common.none)",
|
||||||
|
"replayGainMode_optionTrack": "$t(entity.track, {\"count\": 1})",
|
||||||
|
"replayGainMode": "Modo {{ReplayGain}}",
|
||||||
|
"replayGainPreamp": "Pré-amplificador {{ReplayGain}} (db)",
|
||||||
|
"sampleRate": "Taxa de amostragem",
|
||||||
|
"sidebarPlaylistFolderView_optionNavigation": "Vista de navegação",
|
||||||
|
"sidebarPlaylistMode_optionCompact": "Compacto",
|
||||||
|
"sidebarPlaylistMode_optionExpanded": "Expandido",
|
||||||
|
"sidePlayQueueStyle_optionAttached": "Anexado",
|
||||||
|
"sidePlayQueueLayout_optionHorizontal": "Horizontal",
|
||||||
|
"sidePlayQueueLayout_optionVertical": "Vertical",
|
||||||
|
"startMinimized": "Abrir minimizado",
|
||||||
|
"theme": "Tema",
|
||||||
|
"themeDark": "Tema (escuro)",
|
||||||
|
"transcode": "Ativar transcodificação",
|
||||||
|
"translationTargetLanguage": "Idioma de destino para tradução",
|
||||||
|
"useSystemTheme": "Usar tema do sistema",
|
||||||
|
"queryBuilderCustomFields_inputLabel": "Etiqueta"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"column": {
|
"column": {
|
||||||
"discNumber": "Disco",
|
"discNumber": "Disco",
|
||||||
"size": "$t(common.size)",
|
"size": "Tamanho",
|
||||||
"title": "Titulo"
|
"title": "Titulo",
|
||||||
|
"album": "Álbum",
|
||||||
|
"albumArtist": "Artista do álbum",
|
||||||
|
"albumCount": "Álbuns",
|
||||||
|
"artist": "Artista",
|
||||||
|
"biography": "Bibliografia",
|
||||||
|
"bitDepth": "Profundidade de Bits",
|
||||||
|
"bitrate": "Bitrate",
|
||||||
|
"bpm": "Bpm",
|
||||||
|
"channels": "Canais",
|
||||||
|
"codec": "Codec",
|
||||||
|
"comment": "Comentário",
|
||||||
|
"dateAdded": "Data Adicionada",
|
||||||
|
"favorite": "Favorito",
|
||||||
|
"genre": "Gênero",
|
||||||
|
"lastPlayed": "Última tocada",
|
||||||
|
"path": "Caminho",
|
||||||
|
"playCount": "Reproduções",
|
||||||
|
"rating": "Avaliação",
|
||||||
|
"releaseDate": "Data de Lançamento",
|
||||||
|
"releaseYear": "Ano",
|
||||||
|
"sampleRate": "Taxa de amostragem",
|
||||||
|
"trackNumber": "Faixa",
|
||||||
|
"owner": "Dono"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"label": {
|
"label": {
|
||||||
"discNumber": "Numero do disco",
|
"discNumber": "Numero do disco",
|
||||||
"titleCombined": "$t(common.title) (combinado)"
|
"titleCombined": "$t(common.title) (combinado)",
|
||||||
|
"album": "$t(entity.album, {\"count\": 1})",
|
||||||
|
"albumCount": "$t(entity.album, {\"count\": 2})",
|
||||||
|
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||||
|
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||||
|
"biography": "$t(common.biography)",
|
||||||
|
"bitrate": "$t(common.bitrate)",
|
||||||
|
"bpm": "$t(common.bpm)",
|
||||||
|
"codec": "$t(common.codec)",
|
||||||
|
"composer": "Compositor",
|
||||||
|
"duration": "$t(common.duration)",
|
||||||
|
"favorite": "$t(common.favorite)",
|
||||||
|
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||||
|
"image": "Imagem",
|
||||||
|
"lastPlayed": "Última reprodução",
|
||||||
|
"note": "$t(common.note)",
|
||||||
|
"owner": "$t(common.owner)",
|
||||||
|
"path": "$t(common.path)",
|
||||||
|
"playCount": "Contador de Reprodução",
|
||||||
|
"rating": "$t(common.rating)",
|
||||||
|
"releaseDate": "Data de Lançamento",
|
||||||
|
"size": "$t(common.size)",
|
||||||
|
"songCount": "$t(entity.track, {\"count\": 2})",
|
||||||
|
"title": "$t(common.title)",
|
||||||
|
"year": "$t(common.year)"
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"advancedSettings": "Configurações avançadas",
|
||||||
|
"moveUp": "Mover para cima",
|
||||||
|
"moveDown": "Mover para baixo",
|
||||||
|
"alignLeft": "Alinhar à esquerda",
|
||||||
|
"alignCenter": "Alinhar ao centro",
|
||||||
|
"alignRight": "Alinhar à direita",
|
||||||
|
"displayType": "Tipo do ecrã",
|
||||||
|
"gap": "$t(common.gap)",
|
||||||
|
"size": "$t(common.size)",
|
||||||
|
"size_default": "Predefinição",
|
||||||
|
"size_compact": "Compacto",
|
||||||
|
"size_large": "Grande",
|
||||||
|
"tableColumns": "Colunas das tabelas",
|
||||||
|
"pagination": "Paginação",
|
||||||
|
"pagination_itemsPerPage": "Elementos por página",
|
||||||
|
"pagination_infinite": "Infinito",
|
||||||
|
"pagination_paginate": "Paginado",
|
||||||
|
"showHeader": "Exibir cabeçalho"
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"grid": "Grade"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"filterOperator": {
|
||||||
|
"contains": "Contém",
|
||||||
|
"endsWith": "Termina com",
|
||||||
|
"is": "É",
|
||||||
|
"isNot": "Não é",
|
||||||
|
"isGreaterThan": "É maior que",
|
||||||
|
"isLessThan": "É menor que",
|
||||||
|
"notContains": "Não contém",
|
||||||
|
"startsWith": "Começa com"
|
||||||
|
},
|
||||||
|
"releaseType": {
|
||||||
|
"primary": {
|
||||||
|
"broadcast": "Broadcast",
|
||||||
|
"ep": "EP",
|
||||||
|
"other": "Outros",
|
||||||
|
"single": "Simples"
|
||||||
|
},
|
||||||
|
"secondary": {
|
||||||
|
"compilation": "Compilação",
|
||||||
|
"djMix": "Mixagem de DJ",
|
||||||
|
"demo": "Demo",
|
||||||
|
"interview": "Entrevista",
|
||||||
|
"live": "Ao Vivo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visualizer": {
|
||||||
|
"systemAudioConsentAllow": "Permitir",
|
||||||
|
"systemAudioConsentDecline": "Recusar",
|
||||||
|
"ignoredPresets": "Predefinições Ignoradas",
|
||||||
|
"selectedPresets": "84",
|
||||||
|
"presets": "Predefinições",
|
||||||
|
"applyPreset": "Aplicar Predefinição",
|
||||||
|
"saveAsPreset": "Gravar como Predefinição",
|
||||||
|
"updatePreset": "Atualizar Predefinição",
|
||||||
|
"copyConfiguration": "Copiar Configuração",
|
||||||
|
"pasteConfiguration": "Colar Configuração",
|
||||||
|
"presetNamePlaceholder": "Digite o nome da predefinição",
|
||||||
|
"general": "Geral",
|
||||||
|
"mode": "Modo",
|
||||||
|
"mode1To8": "Modo 1 - 8",
|
||||||
|
"mode10": "Modo 10",
|
||||||
|
"lineWidth": "Largura da Linha",
|
||||||
|
"opacity": "Opacidade",
|
||||||
|
"vertical": "Vertical",
|
||||||
|
"horizontal": "Horizontal",
|
||||||
|
"level": "Nível",
|
||||||
|
"remove": "Remover",
|
||||||
|
"custom": "Personalizado",
|
||||||
|
"builtIn": "Embutido",
|
||||||
|
"colors": "Cores",
|
||||||
|
"gradient": "Gradiente",
|
||||||
|
"smoothing": "Suavizamento",
|
||||||
|
"sensitivity": "Sensibilidade",
|
||||||
|
"gravity": "Gravidade",
|
||||||
|
"radial": "Radial",
|
||||||
|
"radius": "Raio",
|
||||||
|
"options": {
|
||||||
|
"colorMode": {
|
||||||
|
"gradient": "Gradiente"
|
||||||
|
},
|
||||||
|
"gradient": {
|
||||||
|
"classic": "Clássico",
|
||||||
|
"rainbow": "Arco-íris"
|
||||||
|
},
|
||||||
|
"frequencyScale": {
|
||||||
|
"none": "Nenhum"
|
||||||
|
},
|
||||||
|
"weightingFilter": {
|
||||||
|
"none": "Nenhum",
|
||||||
|
"a": "A",
|
||||||
|
"b": "B",
|
||||||
|
"c": "C",
|
||||||
|
"d": "D",
|
||||||
|
"z": "Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -957,7 +957,6 @@
|
|||||||
"artistBackground_description": "Добавляет фоновое изображение для страниц исполнителя, содержащих обложку исполнителя",
|
"artistBackground_description": "Добавляет фоновое изображение для страниц исполнителя, содержащих обложку исполнителя",
|
||||||
"artistBackgroundBlur": "Процент размытия обложки исполнителя",
|
"artistBackgroundBlur": "Процент размытия обложки исполнителя",
|
||||||
"artistBackgroundBlur_description": "Регулирует процент размытия к заднему фону исполнителя",
|
"artistBackgroundBlur_description": "Регулирует процент размытия к заднему фону исполнителя",
|
||||||
"autoDJ_description": "Автоматически добавлять похожие песни в очередь воспроизведения",
|
|
||||||
"autoDJ_itemCount": "Количество элементов",
|
"autoDJ_itemCount": "Количество элементов",
|
||||||
"autoDJ_itemCount_description": "Количество элементов, которые пытаются добавить в очередь при включенной функции автоматического диджеинга",
|
"autoDJ_itemCount_description": "Количество элементов, которые пытаются добавить в очередь при включенной функции автоматического диджеинга",
|
||||||
"autoDJ_timing": "Расчетное время",
|
"autoDJ_timing": "Расчетное время",
|
||||||
|
|||||||
@@ -881,7 +881,6 @@
|
|||||||
"preservePitch": "சுருதியைப் பாதுகாக்கவும்",
|
"preservePitch": "சுருதியைப் பாதுகாக்கவும்",
|
||||||
"preservePitch_description": "பின்னணி வேகத்தை மாற்றும்போது சுருதியைப் பாதுகாக்கிறது",
|
"preservePitch_description": "பின்னணி வேகத்தை மாற்றும்போது சுருதியைப் பாதுகாக்கிறது",
|
||||||
"autoDJ": "ஆட்டோ டி.சே",
|
"autoDJ": "ஆட்டோ டி.சே",
|
||||||
"autoDJ_description": "தானாக வரிசையில் ஒத்த பாடல்களைச் சேர்க்கவும்",
|
|
||||||
"autoDJ_itemCount": "பொருள் எண்ணிக்கை",
|
"autoDJ_itemCount": "பொருள் எண்ணிக்கை",
|
||||||
"autoDJ_itemCount_description": "ஆட்டோ DJ இயக்கப்பட்டிருக்கும் போது, வரிசையில் சேர்க்க முயற்சிக்கும் உருப்படிகளின் எண்ணிக்கை",
|
"autoDJ_itemCount_description": "ஆட்டோ DJ இயக்கப்பட்டிருக்கும் போது, வரிசையில் சேர்க்க முயற்சிக்கும் உருப்படிகளின் எண்ணிக்கை",
|
||||||
"autoDJ_timing": "நேரவிவரம்",
|
"autoDJ_timing": "நேரவிவரம்",
|
||||||
|
|||||||
@@ -508,7 +508,6 @@
|
|||||||
"combinedLyricsAndVisualizer_description": "将歌词和可视化界面合并到同一面板中",
|
"combinedLyricsAndVisualizer_description": "将歌词和可视化界面合并到同一面板中",
|
||||||
"queryBuilderCustomFields_description": "在查询构建器添加自定义字段",
|
"queryBuilderCustomFields_description": "在查询构建器添加自定义字段",
|
||||||
"combinedLyricsAndVisualizer": "在播放器侧边栏合并歌词和可视化界面",
|
"combinedLyricsAndVisualizer": "在播放器侧边栏合并歌词和可视化界面",
|
||||||
"autoDJ_description": "自动添加相似歌曲到队列中",
|
|
||||||
"notify_description": "歌曲变更时显示通知",
|
"notify_description": "歌曲变更时显示通知",
|
||||||
"mpvExtraParameters_description": "向MPV传递额外参数",
|
"mpvExtraParameters_description": "向MPV传递额外参数",
|
||||||
"audioFadeOnStatusChange": "音频改变时淡入淡出",
|
"audioFadeOnStatusChange": "音频改变时淡入淡出",
|
||||||
|
|||||||
@@ -119,7 +119,8 @@
|
|||||||
"newVersionAvailable": "有新的版本可供使用",
|
"newVersionAvailable": "有新的版本可供使用",
|
||||||
"numberOfResults": "{{numberOfResults}} 項結果",
|
"numberOfResults": "{{numberOfResults}} 項結果",
|
||||||
"grouping": "分組",
|
"grouping": "分組",
|
||||||
"back": "返回"
|
"back": "返回",
|
||||||
|
"openFolder": "開啟資料夾"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"endpointNotImplementedError": "{{serverType}} 尚未實現端點 {{endpoint}}",
|
"endpointNotImplementedError": "{{serverType}} 尚未實現端點 {{endpoint}}",
|
||||||
@@ -594,7 +595,7 @@
|
|||||||
"customCssEnable_description": "允許撰寫自訂CSS",
|
"customCssEnable_description": "允許撰寫自訂CSS",
|
||||||
"customCssNotice": "警告:即使已限制某些用法(不允許 URL() 和 content:),但使用自訂 CSS 仍然會透過更改介面帶來風險",
|
"customCssNotice": "警告:即使已限制某些用法(不允許 URL() 和 content:),但使用自訂 CSS 仍然會透過更改介面帶來風險",
|
||||||
"customCss": "自訂CSS",
|
"customCss": "自訂CSS",
|
||||||
"customCss_description": "自訂 CSS 內容。注意:內容和遠端 URL 是不允許使用的屬性。您的內容預覽如下所示。由於需要進行清理,因此存在一些您未設定的其他欄位",
|
"customCss_description": "自訂 CSS 內容。注意:內容和遠端 URL 是不允許使用的屬性。您的內容預覽如下所示。由於需要進行清理,因此存在一些您未設定的其他欄位。桌面端:feishin在應用程式配置目錄中讀取和寫入custom.css,並在檔案更改時重新載入",
|
||||||
"discordPausedStatus": "暫停時顯示 Rich Presence",
|
"discordPausedStatus": "暫停時顯示 Rich Presence",
|
||||||
"discordPausedStatus_description": "啟用後,播放器暫停時將顯示狀態",
|
"discordPausedStatus_description": "啟用後,播放器暫停時將顯示狀態",
|
||||||
"discordListening": "將狀態設為\"正在聽\"",
|
"discordListening": "將狀態設為\"正在聽\"",
|
||||||
@@ -714,9 +715,8 @@
|
|||||||
"playerFilters": "從佇列中過濾歌曲",
|
"playerFilters": "從佇列中過濾歌曲",
|
||||||
"playerFilters_description": "根據以下條件,排除要新增至佇列中的歌曲",
|
"playerFilters_description": "根據以下條件,排除要新增至佇列中的歌曲",
|
||||||
"autoDJ": "Auto DJ",
|
"autoDJ": "Auto DJ",
|
||||||
"autoDJ_description": "自動將相似的歌曲加入到播放佇列",
|
"autoDJ_itemCount": "項目數量",
|
||||||
"autoDJ_itemCount": "歌曲數量",
|
"autoDJ_itemCount_description": "嘗試加入佇列的項目數量",
|
||||||
"autoDJ_itemCount_description": "在啟用Auto DJ時嘗試加入佇列的歌曲數量",
|
|
||||||
"autoDJ_timing_description": "佇列中剩餘多少歌曲時啟動 Auto DJ",
|
"autoDJ_timing_description": "佇列中剩餘多少歌曲時啟動 Auto DJ",
|
||||||
"autoDJ_timing": "觸發時機",
|
"autoDJ_timing": "觸發時機",
|
||||||
"logLevel": "Log等級",
|
"logLevel": "Log等級",
|
||||||
@@ -818,7 +818,16 @@
|
|||||||
"sidebarPlaylistMode_description": "各播放清單在側邊欄列表中的顯示方式",
|
"sidebarPlaylistMode_description": "各播放清單在側邊欄列表中的顯示方式",
|
||||||
"sidebarPlaylistMode": "側邊欄播放清單模式",
|
"sidebarPlaylistMode": "側邊欄播放清單模式",
|
||||||
"sidebarPlaylistMode_optionCompact": "緊湊",
|
"sidebarPlaylistMode_optionCompact": "緊湊",
|
||||||
"sidebarPlaylistMode_optionExpanded": "展開"
|
"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": "相似"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -1137,7 +1146,12 @@
|
|||||||
"input_played": "播放過濾器",
|
"input_played": "播放過濾器",
|
||||||
"input_played_optionAll": "所有曲目",
|
"input_played_optionAll": "所有曲目",
|
||||||
"input_played_optionUnplayed": "僅未播放的曲目",
|
"input_played_optionUnplayed": "僅未播放的曲目",
|
||||||
"input_played_optionPlayed": "僅播放過的曲目"
|
"input_played_optionPlayed": "僅播放過的曲目",
|
||||||
|
"input_kind_albums": "專輯",
|
||||||
|
"input_kind_songs": "歌曲",
|
||||||
|
"input_kind": "隨機選取",
|
||||||
|
"input_limit_albums": "專輯數量?",
|
||||||
|
"input_limit_songs": "歌曲數量?"
|
||||||
},
|
},
|
||||||
"createRadioStation": {
|
"createRadioStation": {
|
||||||
"success": "電台建立成功",
|
"success": "電台建立成功",
|
||||||
@@ -1174,7 +1188,7 @@
|
|||||||
"fieldRecording": "現場錄音",
|
"fieldRecording": "現場錄音",
|
||||||
"demo": "Demo",
|
"demo": "Demo",
|
||||||
"interview": "訪談",
|
"interview": "訪談",
|
||||||
"live": "Live",
|
"live": "現場演出",
|
||||||
"mixtape": "混音帶",
|
"mixtape": "混音帶",
|
||||||
"remix": "Remix",
|
"remix": "Remix",
|
||||||
"soundtrack": "原聲帶",
|
"soundtrack": "原聲帶",
|
||||||
|
|||||||
@@ -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';
|
||||||
|
};
|
||||||
@@ -7,9 +7,10 @@ import { pid } from 'node:process';
|
|||||||
import process from 'process';
|
import process from 'process';
|
||||||
|
|
||||||
import { getMainWindow, sendToastToRenderer } from '../../../index';
|
import { getMainWindow, sendToastToRenderer } from '../../../index';
|
||||||
import { createLog, isMacOS, isWindows } from '../../../utils';
|
import { createLog } from '../../../utils';
|
||||||
import { store } from '../settings';
|
import { store } from '../settings';
|
||||||
|
|
||||||
|
import { isMacOS, isWindows } from '/@/main/env';
|
||||||
import { PlayerData } from '/@/shared/types/domain-types';
|
import { PlayerData } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
declare module 'node-mpv';
|
declare module 'node-mpv';
|
||||||
@@ -119,8 +120,14 @@ const createMpv = async (data: {
|
|||||||
}): Promise<MpvAPI> => {
|
}): Promise<MpvAPI> => {
|
||||||
const { binaryPath, extraParameters, properties } = data;
|
const { binaryPath, extraParameters, properties } = data;
|
||||||
const resolvedBinaryPath = await resolveMpvBinaryPath(binaryPath);
|
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(
|
const mpv = new MpvAPI(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { BrowserWindow, globalShortcut, systemPreferences } from 'electron';
|
import { BrowserWindow, globalShortcut, systemPreferences } from 'electron';
|
||||||
|
|
||||||
import { isLinux, isMacOS } from '../../../utils';
|
|
||||||
import { store } from '../settings';
|
import { store } from '../settings';
|
||||||
|
|
||||||
|
import { isLinux, isMacOS } from '/@/main/env';
|
||||||
import { PlayerType } from '/@/shared/types/types';
|
import { PlayerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
export const enableMediaKeys = (window: BrowserWindow | null) => {
|
export const enableMediaKeys = (window: BrowserWindow | null) => {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { deflate, gzip } from 'zlib';
|
|||||||
|
|
||||||
import manifest from './manifest.json';
|
import manifest from './manifest.json';
|
||||||
|
|
||||||
|
import { isLinux } from '/@/main/env';
|
||||||
import { getMainWindow } from '/@/main/index';
|
import { getMainWindow } from '/@/main/index';
|
||||||
import { isLinux } from '/@/main/utils';
|
|
||||||
import { QueueSong } from '/@/shared/types/domain-types';
|
import { QueueSong } from '/@/shared/types/domain-types';
|
||||||
import { ClientEvent, ServerEvent } from '/@/shared/types/remote-types';
|
import { ClientEvent, ServerEvent } from '/@/shared/types/remote-types';
|
||||||
import { PlayerRepeat, PlayerStatus, SongState } from '/@/shared/types/types';
|
import { PlayerRepeat, PlayerStatus, SongState } from '/@/shared/types/types';
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
import type { TitleTheme } from '/@/shared/types/types';
|
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 Store from 'electron-store';
|
||||||
|
import { promises as fs, watch as fsWatch } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const getFrame = () => {
|
const getFrame = () => {
|
||||||
@@ -26,6 +37,67 @@ const storePath = isDevelopment
|
|||||||
? path.normalize(`${defaultUserDataPath}-dev`)
|
? path.normalize(`${defaultUserDataPath}-dev`)
|
||||||
: path.normalize(defaultUserDataPath);
|
: 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>({
|
export const store = new Store<any>({
|
||||||
beforeEachMigration: (_store, context) => {
|
beforeEachMigration: (_store, context) => {
|
||||||
console.log(`settings migrate from ${context.fromVersion} → ${context.toVersion}`);
|
console.log(`settings migrate from ${context.fromVersion} → ${context.toVersion}`);
|
||||||
@@ -120,3 +192,42 @@ ipcMain.handle('open-file-selector', async (_event, options: OpenDialogOptions)
|
|||||||
|
|
||||||
return result.filePaths[0] || null;
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
+40
-12
@@ -16,6 +16,7 @@ import {
|
|||||||
protocol,
|
protocol,
|
||||||
Rectangle,
|
Rectangle,
|
||||||
screen,
|
screen,
|
||||||
|
session,
|
||||||
shell,
|
shell,
|
||||||
Tray,
|
Tray,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
@@ -33,16 +34,9 @@ import { store } from './features/core/settings';
|
|||||||
import { canHandleVisualizerDisplayMedia } from './features/core/visualizer';
|
import { canHandleVisualizerDisplayMedia } from './features/core/visualizer';
|
||||||
import MenuBuilder, { MenuPlaybackState } from './menu';
|
import MenuBuilder, { MenuPlaybackState } from './menu';
|
||||||
import './features';
|
import './features';
|
||||||
import {
|
import { autoUpdaterLogInterface, createLog, hotkeyToElectronAccelerator } from './utils';
|
||||||
autoUpdaterLogInterface,
|
|
||||||
createLog,
|
|
||||||
disableAutoUpdates,
|
|
||||||
hotkeyToElectronAccelerator,
|
|
||||||
isLinux,
|
|
||||||
isMacOS,
|
|
||||||
isWindows,
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
|
import { disableAutoUpdates, isLinux, isMacOS, isWindows } from '/@/main/env';
|
||||||
import { PlayerRepeat, PlayerStatus, PlayerType, TitleTheme } from '/@/shared/types/types';
|
import { PlayerRepeat, PlayerStatus, PlayerType, TitleTheme } from '/@/shared/types/types';
|
||||||
|
|
||||||
const ALPHA_UPDATER_CONFIG: {
|
const ALPHA_UPDATER_CONFIG: {
|
||||||
@@ -286,6 +280,16 @@ let currentRepeatMode: PlayerRepeat = PlayerRepeat.NONE;
|
|||||||
let currentSidebarCollapsed = false;
|
let currentSidebarCollapsed = false;
|
||||||
let currentShuffleEnabled = false;
|
let currentShuffleEnabled = false;
|
||||||
let playbackMenuAccelerators: MenuPlaybackState['accelerators'] = {};
|
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') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
import('source-map-support').then((sourceMapSupport) => {
|
import('source-map-support').then((sourceMapSupport) => {
|
||||||
@@ -331,7 +335,7 @@ if (isDevelopment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RESOURCES_PATH = app.isPackaged
|
const RESOURCES_PATH = app.isPackaged
|
||||||
? path.join(process.resourcesPath, 'assets')
|
? path.join(path.dirname(app.getAppPath()), 'assets')
|
||||||
: path.join(__dirname, '../../assets');
|
: path.join(__dirname, '../../assets');
|
||||||
|
|
||||||
const getAssetPath = (...paths: string[]): string => {
|
const getAssetPath = (...paths: string[]): string => {
|
||||||
@@ -346,7 +350,7 @@ const rebuildMainMenu = () => {
|
|||||||
if (!menuBuilder || !mainWindow) return;
|
if (!menuBuilder || !mainWindow) return;
|
||||||
|
|
||||||
menuBuilder.buildMenu({
|
menuBuilder.buildMenu({
|
||||||
accelerators: playbackMenuAccelerators,
|
accelerators: inputFocused ? {} : playbackMenuAccelerators,
|
||||||
playbackStatus: currentPlaybackStatus,
|
playbackStatus: currentPlaybackStatus,
|
||||||
privateMode: currentPrivateMode,
|
privateMode: currentPrivateMode,
|
||||||
repeatMode: currentRepeatMode,
|
repeatMode: currentRepeatMode,
|
||||||
@@ -477,6 +481,15 @@ const createTray = () => {
|
|||||||
tray.setContextMenu(contextMenu);
|
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> {
|
async function createWindow(first = true): Promise<void> {
|
||||||
if (isDevelopment) {
|
if (isDevelopment) {
|
||||||
await installExtensions().catch(console.log);
|
await installExtensions().catch(console.log);
|
||||||
@@ -518,7 +531,7 @@ async function createWindow(first = true): Promise<void> {
|
|||||||
devTools: true,
|
devTools: true,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
sandbox: false,
|
sandbox: true,
|
||||||
webSecurity: !store.get('ignore_cors'),
|
webSecurity: !store.get('ignore_cors'),
|
||||||
},
|
},
|
||||||
width: 1440,
|
width: 1440,
|
||||||
@@ -730,7 +743,9 @@ async function createWindow(first = true): Promise<void> {
|
|||||||
|
|
||||||
// Open URLs in the user's browser
|
// Open URLs in the user's browser
|
||||||
mainWindow.webContents.setWindowOpenHandler((edata) => {
|
mainWindow.webContents.setWindowOpenHandler((edata) => {
|
||||||
|
if (validateUrl(edata.url)) {
|
||||||
shell.openExternal(edata.url);
|
shell.openExternal(edata.url);
|
||||||
|
}
|
||||||
return { action: 'deny' };
|
return { action: 'deny' };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -770,7 +785,9 @@ async function createWindow(first = true): Promise<void> {
|
|||||||
nativeTheme.themeSource = theme || 'dark';
|
nativeTheme.themeSource = theme || 'dark';
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
if (validateUrl(details.url)) {
|
||||||
shell.openExternal(details.url);
|
shell.openExternal(details.url);
|
||||||
|
}
|
||||||
return { action: 'deny' };
|
return { action: 'deny' };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1017,6 +1034,17 @@ if (!singleInstance) {
|
|||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||||
|
callback({
|
||||||
|
responseHeaders: {
|
||||||
|
...details.responseHeaders,
|
||||||
|
'Content-Security-Policy': [
|
||||||
|
"script-src 'self' '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();
|
createWindow();
|
||||||
if (store.get('window_enable_tray', true)) {
|
if (store.get('window_enable_tray', true)) {
|
||||||
createTray();
|
createTray();
|
||||||
|
|||||||
@@ -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) => {
|
export const hotkeyToElectronAccelerator = (hotkey: string) => {
|
||||||
let accelerator = hotkey;
|
let accelerator = hotkey;
|
||||||
|
|
||||||
|
|||||||
@@ -8,21 +8,11 @@ const send = (channel: string, ...args: any[]) => {
|
|||||||
ipcRenderer.send(channel, ...args);
|
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) => {
|
const removeListener = (channel: string, listener: (event: any, ...args: any[]) => void) => {
|
||||||
ipcRenderer.removeListener(channel, listener);
|
ipcRenderer.removeListener(channel, listener);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ipc = {
|
export const ipc = {
|
||||||
invoke,
|
|
||||||
on,
|
|
||||||
removeAllListeners,
|
removeAllListeners,
|
||||||
removeListener,
|
removeListener,
|
||||||
send,
|
send,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ipcRenderer, IpcRendererEvent, OpenDialogOptions, webFrame } from 'electron';
|
import { ipcRenderer, OpenDialogOptions, webFrame } from 'electron';
|
||||||
|
|
||||||
import { TitleTheme } from '/@/shared/types/types';
|
import { TitleTheme } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -41,8 +41,8 @@ const setZoomFactor = (zoomFactor: number) => {
|
|||||||
webFrame.setZoomFactor(zoomFactor / 100);
|
webFrame.setZoomFactor(zoomFactor / 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fontError = (cb: (event: IpcRendererEvent, file: string) => void) => {
|
const fontError = (cb: (file: string) => void) => {
|
||||||
ipcRenderer.on('custom-font-error', cb);
|
ipcRenderer.on('custom-font-error', (_, file) => cb(file));
|
||||||
};
|
};
|
||||||
|
|
||||||
const themeSet = (theme: TitleTheme): void => {
|
const themeSet = (theme: TitleTheme): void => {
|
||||||
|
|||||||
+11
-15
@@ -1,4 +1,4 @@
|
|||||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
import { QueueSong } from '/@/shared/types/domain-types';
|
import { QueueSong } from '/@/shared/types/domain-types';
|
||||||
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/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);
|
ipcRenderer.send('update-song', song, imageUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestSeek = (cb: (event: IpcRendererEvent, data: { offset: number }) => void) => {
|
const requestSeek = (cb: (data: { offset: number }) => void) => {
|
||||||
ipcRenderer.on('request-seek', cb);
|
ipcRenderer.on('request-seek', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestPosition = (cb: (event: IpcRendererEvent, data: { position: number }) => void) => {
|
const requestPosition = (cb: (data: { position: number }) => void) => {
|
||||||
ipcRenderer.on('request-position', cb);
|
ipcRenderer.on('request-position', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestToggleRepeat = (
|
const requestToggleRepeat = (cb: (data: { repeat: PlayerRepeat }) => void) => {
|
||||||
cb: (event: IpcRendererEvent, data: { repeat: PlayerRepeat }) => void,
|
ipcRenderer.on('mpris-request-toggle-repeat', (_, data) => cb(data));
|
||||||
) => {
|
|
||||||
ipcRenderer.on('mpris-request-toggle-repeat', cb);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestToggleShuffle = (
|
const requestToggleShuffle = (cb: (data: { shuffle: boolean }) => void) => {
|
||||||
cb: (event: IpcRendererEvent, data: { shuffle: boolean }) => void,
|
ipcRenderer.on('mpris-request-toggle-shuffle', (_, data) => cb(data));
|
||||||
) => {
|
|
||||||
ipcRenderer.on('mpris-request-toggle-shuffle', cb);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestVolume = (cb: (event: IpcRendererEvent, data: { volume: number }) => void) => {
|
const requestVolume = (cb: (data: { volume: number }) => void) => {
|
||||||
ipcRenderer.on('request-volume', cb);
|
ipcRenderer.on('request-volume', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mpris = {
|
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';
|
import { PlayerData } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
@@ -102,76 +102,76 @@ const getAudioDevices = async () => {
|
|||||||
return ipcRenderer.invoke('player-get-audio-devices');
|
return ipcRenderer.invoke('player-get-audio-devices');
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererAutoNext = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererAutoNext = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-auto-next', cb);
|
ipcRenderer.on('renderer-player-auto-next', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererCurrentTime = (cb: (event: IpcRendererEvent, data: number) => void) => {
|
const rendererCurrentTime = (cb: (data: number) => void) => {
|
||||||
ipcRenderer.on('renderer-player-current-time', cb);
|
ipcRenderer.on('renderer-player-current-time', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererNext = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererNext = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-next', cb);
|
ipcRenderer.on('renderer-player-next', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererPause = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererPause = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-pause', cb);
|
ipcRenderer.on('renderer-player-pause', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererPlay = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererPlay = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-play', cb);
|
ipcRenderer.on('renderer-player-play', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererPlayPause = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererPlayPause = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-play-pause', cb);
|
ipcRenderer.on('renderer-player-play-pause', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererPrevious = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererPrevious = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-previous', cb);
|
ipcRenderer.on('renderer-player-previous', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererStop = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererStop = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-stop', cb);
|
ipcRenderer.on('renderer-player-stop', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererSkipForward = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererSkipForward = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-skip-forward', cb);
|
ipcRenderer.on('renderer-player-skip-forward', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererSkipBackward = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererSkipBackward = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-skip-backward', cb);
|
ipcRenderer.on('renderer-player-skip-backward', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererVolumeUp = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererVolumeUp = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-volume-up', cb);
|
ipcRenderer.on('renderer-player-volume-up', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererVolumeDown = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererVolumeDown = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-volume-down', cb);
|
ipcRenderer.on('renderer-player-volume-down', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererVolumeMute = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererVolumeMute = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-volume-mute', cb);
|
ipcRenderer.on('renderer-player-volume-mute', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererToggleRepeat = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererToggleRepeat = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-toggle-repeat', cb);
|
ipcRenderer.on('renderer-player-toggle-repeat', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererToggleShuffle = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererToggleShuffle = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-toggle-shuffle', cb);
|
ipcRenderer.on('renderer-player-toggle-shuffle', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererQuit = (cb: (event: IpcRendererEvent) => void) => {
|
const rendererQuit = (cb: () => void) => {
|
||||||
ipcRenderer.on('renderer-player-quit', cb);
|
ipcRenderer.on('renderer-player-quit', () => cb());
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererError = (cb: (event: IpcRendererEvent, data: string) => void) => {
|
const rendererError = (cb: (data: string) => void) => {
|
||||||
ipcRenderer.on('renderer-player-error', cb);
|
ipcRenderer.on('renderer-player-error', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererPlayerFallback = (cb: (event: IpcRendererEvent, data: boolean) => void) => {
|
const rendererPlayerFallback = (cb: (data: boolean) => void) => {
|
||||||
ipcRenderer.on('renderer-player-fallback', cb);
|
ipcRenderer.on('renderer-player-fallback', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mpvPlayer = {
|
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 { QueueSong } from '/@/shared/types/domain-types';
|
||||||
import { PlayerStatus } from '/@/shared/types/types';
|
import { PlayerStatus } from '/@/shared/types/types';
|
||||||
|
|
||||||
const requestFavorite = (
|
const requestFavorite = (
|
||||||
cb: (
|
cb: (data: { favorite: boolean; id: string; serverId: string }) => void,
|
||||||
event: IpcRendererEvent,
|
|
||||||
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) => {
|
const requestPosition = (cb: (data: { position: number }) => void) => {
|
||||||
ipcRenderer.on('request-position', cb);
|
ipcRenderer.on('request-position', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestRating = (
|
const requestRating = (cb: (data: { id: string; rating: number; serverId: string }) => void) => {
|
||||||
cb: (event: IpcRendererEvent, data: { id: string; rating: number; serverId: string }) => void,
|
ipcRenderer.on('request-rating', (_, data) => cb(data));
|
||||||
) => {
|
|
||||||
ipcRenderer.on('request-rating', cb);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestSeek = (cb: (event: IpcRendererEvent, data: { offset: number }) => void) => {
|
const requestSeek = (cb: (data: { offset: number }) => void) => {
|
||||||
ipcRenderer.on('request-seek', cb);
|
ipcRenderer.on('request-seek', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestVolume = (cb: (event: IpcRendererEvent, data: { volume: number }) => void) => {
|
const requestVolume = (cb: (data: { volume: number }) => void) => {
|
||||||
ipcRenderer.on('request-volume', cb);
|
ipcRenderer.on('request-volume', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
const setRemoteEnabled = (enabled: boolean): Promise<null | string> => {
|
const setRemoteEnabled = (enabled: boolean): Promise<null | string> => {
|
||||||
|
|||||||
+71
-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) => {
|
const openItem = async (path: string) => {
|
||||||
return ipcRenderer.invoke('open-item', path);
|
return ipcRenderer.invoke('open-item', path);
|
||||||
@@ -10,29 +10,44 @@ const openApplicationDirectory = async () => {
|
|||||||
return ipcRenderer.invoke('open-application-directory');
|
return ipcRenderer.invoke('open-application-directory');
|
||||||
};
|
};
|
||||||
|
|
||||||
const playerErrorListener = (cb: (event: IpcRendererEvent, data: { code: number }) => void) => {
|
const getCustomCss = async (): Promise<
|
||||||
ipcRenderer.on('player-error-listener', cb);
|
| 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 = (
|
const mainMessageListener = (
|
||||||
cb: (
|
cb: (data: { message: string; type: 'error' | 'info' | 'success' | 'warning' }) => void,
|
||||||
event: IpcRendererEvent,
|
|
||||||
data: { message: string; type: 'error' | 'info' | 'success' | 'warning' },
|
|
||||||
) => void,
|
|
||||||
) => {
|
) => {
|
||||||
ipcRenderer.on('toast-from-main', cb);
|
ipcRenderer.on('toast-from-main', (_, data) => cb(data));
|
||||||
};
|
|
||||||
|
|
||||||
const logger = (
|
|
||||||
cb: (
|
|
||||||
event: IpcRendererEvent,
|
|
||||||
data: {
|
|
||||||
message: string;
|
|
||||||
type: 'debug' | 'error' | 'info' | 'verbose' | 'warning';
|
|
||||||
},
|
|
||||||
) => void,
|
|
||||||
) => {
|
|
||||||
ipcRenderer.send('logger', cb);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const download = (url: string) => {
|
const download = (url: string) => {
|
||||||
@@ -43,6 +58,14 @@ const checkForUpdates = (): Promise<{ updateAvailable: boolean; version?: string
|
|||||||
return ipcRenderer.invoke('app-check-for-updates');
|
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 => {
|
const forceGarbageCollection = (): boolean => {
|
||||||
try {
|
try {
|
||||||
if (typeof global.gc === 'function') {
|
if (typeof global.gc === 'function') {
|
||||||
@@ -61,41 +84,51 @@ const forceGarbageCollection = (): boolean => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererOpenSettings = (cb: (event: IpcRendererEvent) => void) => {
|
const setInputFocused = (focused: boolean) => {
|
||||||
ipcRenderer.on('renderer-open-settings', cb);
|
ipcRenderer.send('input-focus-state', focused);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererOpenCommandPalette = (cb: (event: IpcRendererEvent) => void) => {
|
const rendererOpenSettings = (cb: () => void) => {
|
||||||
ipcRenderer.on('renderer-open-command-palette', cb);
|
ipcRenderer.on('renderer-open-settings', () => cb());
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererOpenManageServers = (cb: (event: IpcRendererEvent) => void) => {
|
const rendererOpenCommandPalette = (cb: () => void) => {
|
||||||
ipcRenderer.on('renderer-open-manage-servers', cb);
|
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);
|
ipcRenderer.on('renderer-toggle-private-mode', cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererToggleSidebar = (cb: (event: IpcRendererEvent) => void) => {
|
const rendererToggleSidebar = (cb: () => void) => {
|
||||||
ipcRenderer.on('renderer-toggle-sidebar', cb);
|
ipcRenderer.on('renderer-toggle-sidebar', () => cb());
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererOpenReleaseNotes = (cb: (event: IpcRendererEvent) => void) => {
|
const rendererOpenReleaseNotes = (cb: () => void) => {
|
||||||
ipcRenderer.on('renderer-open-release-notes', cb);
|
ipcRenderer.on('renderer-open-release-notes', () => cb());
|
||||||
|
};
|
||||||
|
|
||||||
|
const rendererUpdateAvailable = (cb: (version: string) => void) => {
|
||||||
|
ipcRenderer.on('update-available', (_, version) => cb(version));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const utils = {
|
export const utils = {
|
||||||
checkForUpdates,
|
checkForUpdates,
|
||||||
|
customCssUpdatedListener,
|
||||||
disableAutoUpdates,
|
disableAutoUpdates,
|
||||||
download,
|
download,
|
||||||
forceGarbageCollection,
|
forceGarbageCollection,
|
||||||
|
getCustomCss,
|
||||||
isLinux,
|
isLinux,
|
||||||
isMacOS,
|
isMacOS,
|
||||||
isWindows,
|
isWindows,
|
||||||
logger,
|
|
||||||
mainMessageListener,
|
mainMessageListener,
|
||||||
openApplicationDirectory,
|
openApplicationDirectory,
|
||||||
|
openCustomCssFolder,
|
||||||
openItem,
|
openItem,
|
||||||
playerErrorListener,
|
playerErrorListener,
|
||||||
rendererOpenCommandPalette,
|
rendererOpenCommandPalette,
|
||||||
@@ -104,6 +137,11 @@ export const utils = {
|
|||||||
rendererOpenSettings,
|
rendererOpenSettings,
|
||||||
rendererTogglePrivateMode,
|
rendererTogglePrivateMode,
|
||||||
rendererToggleSidebar,
|
rendererToggleSidebar,
|
||||||
|
rendererUpdateAvailable,
|
||||||
|
saveCustomCss,
|
||||||
|
setInputFocused,
|
||||||
|
startPowerSaveBlocker,
|
||||||
|
stopPowerSaveBlocker,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Utils = typeof utils;
|
export type Utils = typeof utils;
|
||||||
|
|||||||
@@ -411,8 +411,12 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
throw new Error('Failed to get album detail');
|
throw new Error('Failed to get album detail');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(
|
return jfNormalize.album(
|
||||||
{ ...res.body, Songs: songsRes.body.Items },
|
{ ...res.body, Songs: songs },
|
||||||
apiClientProps.server,
|
apiClientProps.server,
|
||||||
args.context?.pathReplace,
|
args.context?.pathReplace,
|
||||||
args.context?.pathReplaceWith,
|
args.context?.pathReplaceWith,
|
||||||
|
|||||||
+113
-1
@@ -15,7 +15,12 @@ import { useCheckForUpdates } from '/@/renderer/hooks/use-check-for-updates';
|
|||||||
import { useNativeMenuSync } from '/@/renderer/hooks/use-native-menu-sync';
|
import { useNativeMenuSync } from '/@/renderer/hooks/use-native-menu-sync';
|
||||||
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
|
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
|
||||||
import { AppRouter } from '/@/renderer/router/app-router';
|
import { AppRouter } from '/@/renderer/router/app-router';
|
||||||
import { useCssSettings, useHotkeySettings, useLanguage } from '/@/renderer/store';
|
import {
|
||||||
|
useCssSettings,
|
||||||
|
useHotkeySettings,
|
||||||
|
useLanguage,
|
||||||
|
useSettingsStoreActions,
|
||||||
|
} from '/@/renderer/store';
|
||||||
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
|
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
|
||||||
import { sanitizeCss } from '/@/renderer/utils/sanitize';
|
import { sanitizeCss } from '/@/renderer/utils/sanitize';
|
||||||
import { WebAudio } from '/@/shared/types/types';
|
import { WebAudio } from '/@/shared/types/types';
|
||||||
@@ -31,6 +36,7 @@ const UpdateAvailableDialog = lazy(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const ipc = isElectron() ? window.api.ipc : null;
|
const ipc = isElectron() ? window.api.ipc : null;
|
||||||
|
const utils = isElectron() ? window.api.utils : null;
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
return <ThemedApp />;
|
return <ThemedApp />;
|
||||||
@@ -89,10 +95,12 @@ const AppEffects = () => (
|
|||||||
<>
|
<>
|
||||||
<SyncSettingsEffect />
|
<SyncSettingsEffect />
|
||||||
<UpdateCheckEffect />
|
<UpdateCheckEffect />
|
||||||
|
<CustomCssFileEffect />
|
||||||
<CssSettingsEffect />
|
<CssSettingsEffect />
|
||||||
<GlobalShortcutsEffect />
|
<GlobalShortcutsEffect />
|
||||||
<LanguageEffect />
|
<LanguageEffect />
|
||||||
<NativeMenuSyncEffect />
|
<NativeMenuSyncEffect />
|
||||||
|
<InputFocusEffect />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -141,6 +149,71 @@ const CssSettingsEffect = () => {
|
|||||||
return null;
|
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 GlobalShortcutsEffect = () => {
|
||||||
const { bindings } = useHotkeySettings();
|
const { bindings } = useHotkeySettings();
|
||||||
|
|
||||||
@@ -170,3 +243,42 @@ const NativeMenuSyncEffect = () => {
|
|||||||
|
|
||||||
return null;
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { FavoriteColumn } from '/@/renderer/components/item-list/item-table-list
|
|||||||
import { GenreBadgeColumn } from '/@/renderer/components/item-list/item-table-list/columns/genre-badge-column';
|
import { GenreBadgeColumn } from '/@/renderer/components/item-list/item-table-list/columns/genre-badge-column';
|
||||||
import { GenreColumn } from '/@/renderer/components/item-list/item-table-list/columns/genre-column';
|
import { GenreColumn } from '/@/renderer/components/item-list/item-table-list/columns/genre-column';
|
||||||
import { ImageColumn } from '/@/renderer/components/item-list/item-table-list/columns/image-column';
|
import { ImageColumn } from '/@/renderer/components/item-list/item-table-list/columns/image-column';
|
||||||
|
import { NumericColumn } from '/@/renderer/components/item-list/item-table-list/columns/numeric-column';
|
||||||
import { PathColumn } from '/@/renderer/components/item-list/item-table-list/columns/path-column';
|
import { PathColumn } from '/@/renderer/components/item-list/item-table-list/columns/path-column';
|
||||||
import { PlaylistReorderColumn } from '/@/renderer/components/item-list/item-table-list/columns/playlist-reorder-column';
|
import { PlaylistReorderColumn } from '/@/renderer/components/item-list/item-table-list/columns/playlist-reorder-column';
|
||||||
import { RatingColumn } from '/@/renderer/components/item-list/item-table-list/columns/rating-column';
|
import { RatingColumn } from '/@/renderer/components/item-list/item-table-list/columns/rating-column';
|
||||||
@@ -239,10 +240,7 @@ const ItemTableListColumnBase = (props: ItemTableListColumn) => {
|
|||||||
case TableColumn.CHANNELS:
|
case TableColumn.CHANNELS:
|
||||||
case TableColumn.DISC_NUMBER:
|
case TableColumn.DISC_NUMBER:
|
||||||
case TableColumn.SAMPLE_RATE:
|
case TableColumn.SAMPLE_RATE:
|
||||||
case TableColumn.TRACK_NUMBER:
|
return <NumericColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||||
return (
|
|
||||||
<TrackNumberColumn {...props} {...dragProps} controls={controls} type={type} />
|
|
||||||
);
|
|
||||||
|
|
||||||
case TableColumn.COMPOSER:
|
case TableColumn.COMPOSER:
|
||||||
return <ComposerColumn {...props} {...dragProps} controls={controls} type={type} />;
|
return <ComposerColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||||
@@ -304,6 +302,11 @@ const ItemTableListColumnBase = (props: ItemTableListColumn) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case TableColumn.TRACK_NUMBER:
|
||||||
|
return (
|
||||||
|
<TrackNumberColumn {...props} {...dragProps} controls={controls} type={type} />
|
||||||
|
);
|
||||||
|
|
||||||
case TableColumn.USER_FAVORITE:
|
case TableColumn.USER_FAVORITE:
|
||||||
return <FavoriteColumn {...props} {...dragProps} controls={controls} type={type} />;
|
return <FavoriteColumn {...props} {...dragProps} controls={controls} type={type} />;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||||
import { useCurrentServerId, usePlayButtonBehavior } from '/@/renderer/store';
|
import { useArtistRadioCount, useCurrentServerId, usePlayButtonBehavior } from '/@/renderer/store';
|
||||||
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
||||||
import { Song } from '/@/shared/types/domain-types';
|
import { Song } from '/@/shared/types/domain-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
@@ -27,6 +27,8 @@ export const PlayTrackRadioAction = ({
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
|
||||||
|
const radioCount = useArtistRadioCount();
|
||||||
|
|
||||||
const handlePlayTrackRadio = useCallback(
|
const handlePlayTrackRadio = useCallback(
|
||||||
async (playType: Play) => {
|
async (playType: Play) => {
|
||||||
if (!serverId || !song) return;
|
if (!serverId || !song) return;
|
||||||
@@ -35,6 +37,7 @@ export const PlayTrackRadioAction = ({
|
|||||||
const similarSongs = await queryClient.fetchQuery({
|
const similarSongs = await queryClient.fetchQuery({
|
||||||
...songsQueries.similar({
|
...songsQueries.similar({
|
||||||
query: {
|
query: {
|
||||||
|
count: radioCount,
|
||||||
songId: song.id,
|
songId: song.id,
|
||||||
},
|
},
|
||||||
serverId,
|
serverId,
|
||||||
@@ -53,7 +56,7 @@ export const PlayTrackRadioAction = ({
|
|||||||
console.error('Failed to load track radio:', error);
|
console.error('Failed to load track radio:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[player, queryClient, serverId, skipFirstSong, song],
|
[player, queryClient, radioCount, serverId, skipFirstSong, song],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePlayTrackRadioNow = useCallback(() => {
|
const handlePlayTrackRadioNow = useCallback(() => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||||
|
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||||
import {
|
import {
|
||||||
useIsRadioActive,
|
useIsRadioActive,
|
||||||
useRadioPlayer,
|
useRadioPlayer,
|
||||||
@@ -36,6 +37,7 @@ const DiscordStatusDisplayType = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type ActivityState = [QueueSong | undefined, number, PlayerStatus];
|
type ActivityState = [QueueSong | undefined, number, PlayerStatus];
|
||||||
|
type ActivityTrigger = 'initial' | 'interval' | 'seek' | 'status_change' | 'track_change';
|
||||||
|
|
||||||
const MAX_FIELD_LENGTH = 127;
|
const MAX_FIELD_LENGTH = 127;
|
||||||
const MAX_URL_LENGTH = 256;
|
const MAX_URL_LENGTH = 256;
|
||||||
@@ -64,22 +66,24 @@ export const useDiscordRpc = () => {
|
|||||||
const imageUrlRef = useRef<null | string | undefined>(imageUrl);
|
const imageUrlRef = useRef<null | string | undefined>(imageUrl);
|
||||||
const previousEnabledRef = useRef<boolean>(discordSettings.enabled);
|
const previousEnabledRef = useRef<boolean>(discordSettings.enabled);
|
||||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const previousActivityStateRef = useRef<ActivityState | null>(null);
|
const discordEnabledRef = useRef<boolean>(discordSettings.enabled);
|
||||||
|
const privateModeRef = useRef<boolean>(privateMode);
|
||||||
|
|
||||||
// Update imageUrl ref when it changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
imageUrlRef.current = imageUrl;
|
imageUrlRef.current = imageUrl;
|
||||||
}, [imageUrl]);
|
}, [imageUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
discordEnabledRef.current = discordSettings.enabled;
|
||||||
|
}, [discordSettings.enabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
privateModeRef.current = privateMode;
|
||||||
|
}, [privateMode]);
|
||||||
|
|
||||||
const setActivity = useCallback(
|
const setActivity = useCallback(
|
||||||
async (current: ActivityState, previous: ActivityState) => {
|
async (current: ActivityState, trigger: ActivityTrigger) => {
|
||||||
// Check if track changed by comparing with previous state
|
|
||||||
const song = current[0];
|
const song = current[0];
|
||||||
const previousSong = previous[0];
|
|
||||||
const trackChangedByState =
|
|
||||||
song && previousSong
|
|
||||||
? song._uniqueId !== previousSong._uniqueId
|
|
||||||
: song !== previousSong;
|
|
||||||
const trackChanged = song ? lastUniqueId !== song._uniqueId : false;
|
const trackChanged = song ? lastUniqueId !== song._uniqueId : false;
|
||||||
|
|
||||||
const isPlayingRadio = isRadioActive && isRadioPlaying;
|
const isPlayingRadio = isRadioActive && isRadioPlaying;
|
||||||
@@ -103,6 +107,7 @@ export const useDiscordRpc = () => {
|
|||||||
meta: {
|
meta: {
|
||||||
reason,
|
reason,
|
||||||
status: current[2],
|
status: current[2],
|
||||||
|
trigger,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return discordRpc?.clearActivity();
|
return discordRpc?.clearActivity();
|
||||||
@@ -152,6 +157,7 @@ export const useDiscordRpc = () => {
|
|||||||
showAsListening: discordSettings.showAsListening,
|
showAsListening: discordSettings.showAsListening,
|
||||||
stationName: stationName || 'Radio',
|
stationName: stationName || 'Radio',
|
||||||
title,
|
title,
|
||||||
|
trigger,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
discordRpc?.setActivity(activity);
|
discordRpc?.setActivity(activity);
|
||||||
@@ -162,20 +168,7 @@ export const useDiscordRpc = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
if (trackChanged) {
|
||||||
1. If the song has just started, update status
|
|
||||||
2. If we jump more then 1.2 seconds from last state, update status to match
|
|
||||||
3. If the current song id is completely different, update status
|
|
||||||
4. If the player state changed, update status
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
previous[1] === 0 ||
|
|
||||||
Math.abs(current[1] - previous[1]) > 1.2 ||
|
|
||||||
trackChangedByState ||
|
|
||||||
trackChanged ||
|
|
||||||
current[2] !== previous[2]
|
|
||||||
) {
|
|
||||||
if (trackChangedByState || trackChanged) {
|
|
||||||
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcTrackChanged, {
|
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcTrackChanged, {
|
||||||
category: LogCategory.EXTERNAL,
|
category: LogCategory.EXTERNAL,
|
||||||
meta: {
|
meta: {
|
||||||
@@ -187,17 +180,7 @@ export const useDiscordRpc = () => {
|
|||||||
setlastUniqueId(song._uniqueId);
|
setlastUniqueId(song._uniqueId);
|
||||||
}
|
}
|
||||||
|
|
||||||
let reason: string;
|
const reason = trigger;
|
||||||
if (trackChangedByState || trackChanged) {
|
|
||||||
reason = 'track_changed';
|
|
||||||
} else if (previous[1] === 0) {
|
|
||||||
reason = 'song_started';
|
|
||||||
} else if (Math.abs(current[1] - previous[1]) > 1.2) {
|
|
||||||
reason = 'time_jump';
|
|
||||||
} else {
|
|
||||||
reason = 'player_state_changed';
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = Math.round(Date.now() - current[1] * 1000);
|
const start = Math.round(Date.now() - current[1] * 1000);
|
||||||
const end = Math.round(start + song.duration);
|
const end = Math.round(start + song.duration);
|
||||||
|
|
||||||
@@ -348,28 +331,14 @@ export const useDiscordRpc = () => {
|
|||||||
displayType: discordSettings.displayType,
|
displayType: discordSettings.displayType,
|
||||||
hasLargeImage: !!activity.largeImageKey,
|
hasLargeImage: !!activity.largeImageKey,
|
||||||
hasTimestamps: !!(activity.startTimestamp && activity.endTimestamp),
|
hasTimestamps: !!(activity.startTimestamp && activity.endTimestamp),
|
||||||
previousStatus: previous[2],
|
|
||||||
previousTime: previous[1],
|
|
||||||
reason,
|
reason,
|
||||||
showAsListening: discordSettings.showAsListening,
|
showAsListening: discordSettings.showAsListening,
|
||||||
songName: song.name,
|
songName: song.name,
|
||||||
trackChanged: trackChangedByState || trackChanged,
|
trackChanged,
|
||||||
|
trigger,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
discordRpc?.setActivity(activity);
|
discordRpc?.setActivity(activity);
|
||||||
} else {
|
|
||||||
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcUpdateSkipped, {
|
|
||||||
category: LogCategory.EXTERNAL,
|
|
||||||
meta: {
|
|
||||||
currentStatus: current[2],
|
|
||||||
currentTime: current[1],
|
|
||||||
previousStatus: previous[2],
|
|
||||||
previousTime: previous[1],
|
|
||||||
timeDiff: Math.abs(current[1] - previous[1]),
|
|
||||||
trackChanged: trackChangedByState || trackChanged,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
discordSettings.showAsListening,
|
discordSettings.showAsListening,
|
||||||
@@ -390,7 +359,7 @@ export const useDiscordRpc = () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedSetActivity = useDebouncedCallback(setActivity, 500);
|
const debouncedSetActivity = useDebouncedCallback(setActivity, 1000);
|
||||||
|
|
||||||
// Quit Discord RPC if it was enabled and is now disabled
|
// Quit Discord RPC if it was enabled and is now disabled
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -409,95 +378,110 @@ export const useDiscordRpc = () => {
|
|||||||
}
|
}
|
||||||
}, [discordSettings.clientId, privateMode, discordSettings.enabled]);
|
}, [discordSettings.clientId, privateMode, discordSettings.enabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
const getCurrentActivityState = useCallback((): ActivityState => {
|
||||||
if (!discordSettings.enabled || privateMode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCurrentActivityState = (): ActivityState => {
|
|
||||||
const state = usePlayerStore.getState();
|
const state = usePlayerStore.getState();
|
||||||
const currentSong = state.getCurrentSong();
|
return [
|
||||||
const currentTime = useTimestampStoreBase.getState().timestamp;
|
state.getCurrentSong(),
|
||||||
const status = state.player.status;
|
useTimestampStoreBase.getState().timestamp,
|
||||||
return [currentSong, currentTime, status];
|
state.player.status,
|
||||||
};
|
|
||||||
|
|
||||||
const resetInterval = () => {
|
|
||||||
if (intervalRef.current) {
|
|
||||||
clearInterval(intervalRef.current);
|
|
||||||
}
|
|
||||||
intervalRef.current = setInterval(() => {
|
|
||||||
const current = getCurrentActivityState();
|
|
||||||
const previous = previousActivityStateRef.current || current;
|
|
||||||
debouncedSetActivity(current, previous);
|
|
||||||
previousActivityStateRef.current = current;
|
|
||||||
}, 15000);
|
|
||||||
};
|
|
||||||
|
|
||||||
resetInterval();
|
|
||||||
|
|
||||||
const initialState = getCurrentActivityState();
|
|
||||||
let previousUniqueId = initialState[0]?._uniqueId || '';
|
|
||||||
|
|
||||||
previousActivityStateRef.current = initialState;
|
|
||||||
|
|
||||||
// Set activity immediately when Discord RPC is enabled
|
|
||||||
debouncedSetActivity(initialState, initialState);
|
|
||||||
|
|
||||||
const unsubSongChange = usePlayerStore.subscribe(
|
|
||||||
(state): ActivityState => {
|
|
||||||
const currentSong = state.getCurrentSong();
|
|
||||||
const currentTime = useTimestampStoreBase.getState().timestamp;
|
|
||||||
const status = state.player.status;
|
|
||||||
|
|
||||||
return [currentSong, currentTime, status];
|
|
||||||
},
|
|
||||||
(current, previous) => {
|
|
||||||
const currentUniqueId = current[0]?._uniqueId || '';
|
|
||||||
const trackChanged = previousUniqueId !== currentUniqueId;
|
|
||||||
|
|
||||||
if (trackChanged && current[0]) {
|
|
||||||
resetInterval();
|
|
||||||
previousUniqueId = currentUniqueId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activity: ActivityState = [
|
|
||||||
current[0] as QueueSong,
|
|
||||||
current[1] as number,
|
|
||||||
current[2] as PlayerStatus,
|
|
||||||
];
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Use the ref as the source of truth for previous state
|
const clearRefreshInterval = useCallback(() => {
|
||||||
const previousActivity: ActivityState =
|
|
||||||
previousActivityStateRef.current ||
|
|
||||||
(previous
|
|
||||||
? [
|
|
||||||
previous[0] as QueueSong,
|
|
||||||
previous[1] as number,
|
|
||||||
previous[2] as PlayerStatus,
|
|
||||||
]
|
|
||||||
: activity);
|
|
||||||
|
|
||||||
debouncedSetActivity(activity, previousActivity);
|
|
||||||
|
|
||||||
previousActivityStateRef.current = activity;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubSongChange();
|
|
||||||
if (intervalRef.current) {
|
if (intervalRef.current) {
|
||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
intervalRef.current = null;
|
intervalRef.current = null;
|
||||||
}
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const emitActivityUpdateRef = useRef<(next: ActivityState, trigger: ActivityTrigger) => void>(
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetRefreshInterval = useCallback(() => {
|
||||||
|
clearRefreshInterval();
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
const current = getCurrentActivityState();
|
||||||
|
emitActivityUpdateRef.current(current, 'interval');
|
||||||
|
}, 15000);
|
||||||
|
}, [clearRefreshInterval, getCurrentActivityState]);
|
||||||
|
|
||||||
|
const emitActivityUpdate = useCallback(
|
||||||
|
(next: ActivityState, trigger: ActivityTrigger) => {
|
||||||
|
debouncedSetActivity(next, trigger);
|
||||||
|
resetRefreshInterval();
|
||||||
|
},
|
||||||
|
[debouncedSetActivity, resetRefreshInterval],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
emitActivityUpdateRef.current = emitActivityUpdate;
|
||||||
|
}, [emitActivityUpdate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!discordSettings.enabled || privateMode) {
|
||||||
|
clearRefreshInterval();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = getCurrentActivityState();
|
||||||
|
emitActivityUpdate(initialState, 'initial');
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearRefreshInterval();
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
debouncedSetActivity,
|
clearRefreshInterval,
|
||||||
discordSettings.clientId,
|
|
||||||
discordSettings.enabled,
|
discordSettings.enabled,
|
||||||
|
emitActivityUpdate,
|
||||||
|
getCurrentActivityState,
|
||||||
privateMode,
|
privateMode,
|
||||||
setActivity,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
usePlayerEvents(
|
||||||
|
{
|
||||||
|
onCurrentSongChange: ({ song }) => {
|
||||||
|
if (!discordEnabledRef.current || privateModeRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerState = usePlayerStore.getState();
|
||||||
|
const activityState: ActivityState = [
|
||||||
|
song,
|
||||||
|
useTimestampStoreBase.getState().timestamp,
|
||||||
|
playerState.player.status,
|
||||||
|
];
|
||||||
|
emitActivityUpdateRef.current(activityState, 'track_change');
|
||||||
|
},
|
||||||
|
onPlayerSeekToTimestamp: ({ timestamp }) => {
|
||||||
|
if (!discordEnabledRef.current || privateModeRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerState = usePlayerStore.getState();
|
||||||
|
const activityState: ActivityState = [
|
||||||
|
playerState.getCurrentSong(),
|
||||||
|
timestamp,
|
||||||
|
playerState.player.status,
|
||||||
|
];
|
||||||
|
emitActivityUpdateRef.current(activityState, 'seek');
|
||||||
|
},
|
||||||
|
onPlayerStatus: ({ status }) => {
|
||||||
|
if (!discordEnabledRef.current || privateModeRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerState = usePlayerStore.getState();
|
||||||
|
const activityState: ActivityState = [
|
||||||
|
playerState.getCurrentSong(),
|
||||||
|
useTimestampStoreBase.getState().timestamp,
|
||||||
|
status,
|
||||||
|
];
|
||||||
|
emitActivityUpdateRef.current(activityState, 'status_change');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DiscordRpcHookInner = () => {
|
const DiscordRpcHookInner = () => {
|
||||||
|
|||||||
@@ -214,7 +214,14 @@ export const SidebarPlayQueue = () => {
|
|||||||
))}
|
))}
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
) : (
|
) : (
|
||||||
<Stack gap={0} h="100%" w="100%">
|
<Stack
|
||||||
|
gap={0}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
}}
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
<PlayQueueListControls
|
<PlayQueueListControls
|
||||||
handleSearch={setSearch}
|
handleSearch={setSearch}
|
||||||
searchTerm={search}
|
searchTerm={search}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export const useMainPlayerListener = () => {
|
|||||||
decreaseVolume(volumeWheelStep);
|
decreaseVolume(volumeWheelStep);
|
||||||
});
|
});
|
||||||
|
|
||||||
mpvPlayerListener.rendererError((_event: any, message: string) => {
|
mpvPlayerListener.rendererError((message: string) => {
|
||||||
handleMpvError(message);
|
handleMpvError(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { QueueSong } from '/@/shared/types/domain-types';
|
|||||||
export function useSongUrl(
|
export function useSongUrl(
|
||||||
song: QueueSong | undefined,
|
song: QueueSong | undefined,
|
||||||
current: boolean,
|
current: boolean,
|
||||||
transcode: TranscodingConfig,
|
transcode: Partial<TranscodingConfig>,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const prior = useRef(['', '']);
|
const prior = useRef(['', '']);
|
||||||
const shouldReusePrior = Boolean(
|
const shouldReusePrior = Boolean(
|
||||||
@@ -24,7 +24,7 @@ export function useSongUrl(
|
|||||||
bitrate: transcode.bitrate,
|
bitrate: transcode.bitrate,
|
||||||
format: transcode.format,
|
format: transcode.format,
|
||||||
id: song!.id,
|
id: song!.id,
|
||||||
transcode: transcode.enabled,
|
transcode: transcode.enabled ?? false,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
queryKey: [
|
queryKey: [
|
||||||
@@ -63,7 +63,7 @@ export function useSongUrl(
|
|||||||
|
|
||||||
export const getSongUrl = async (
|
export const getSongUrl = async (
|
||||||
song: QueueSong,
|
song: QueueSong,
|
||||||
transcode: TranscodingConfig,
|
transcode: Partial<TranscodingConfig>,
|
||||||
skipAutoTranscode?: boolean,
|
skipAutoTranscode?: boolean,
|
||||||
) => {
|
) => {
|
||||||
const url = await api.controller.getStreamUrl({
|
const url = await api.controller.getStreamUrl({
|
||||||
@@ -73,7 +73,7 @@ export const getSongUrl = async (
|
|||||||
format: transcode.format,
|
format: transcode.format,
|
||||||
id: song.id,
|
id: song.id,
|
||||||
skipAutoTranscode,
|
skipAutoTranscode,
|
||||||
transcode: transcode.enabled,
|
transcode: transcode.enabled ?? false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ export function WebPlayer() {
|
|||||||
gaplessHandler({
|
gaplessHandler({
|
||||||
currentTime: e.playedSeconds,
|
currentTime: e.playedSeconds,
|
||||||
duration: getDuration(playerRef.current.player1().ref),
|
duration: getDuration(playerRef.current.player1().ref),
|
||||||
|
hasNextSong: Boolean(player2),
|
||||||
isFlac: false,
|
isFlac: false,
|
||||||
isTransitioning,
|
isTransitioning,
|
||||||
nextPlayer: playerRef.current.player2(),
|
nextPlayer: playerRef.current.player2(),
|
||||||
@@ -206,6 +207,7 @@ export function WebPlayer() {
|
|||||||
gaplessHandler({
|
gaplessHandler({
|
||||||
currentTime: e.playedSeconds,
|
currentTime: e.playedSeconds,
|
||||||
duration: getDuration(playerRef.current.player2().ref),
|
duration: getDuration(playerRef.current.player2().ref),
|
||||||
|
hasNextSong: Boolean(player1),
|
||||||
isFlac: false,
|
isFlac: false,
|
||||||
isTransitioning,
|
isTransitioning,
|
||||||
nextPlayer: playerRef.current.player1(),
|
nextPlayer: playerRef.current.player1(),
|
||||||
@@ -680,6 +682,7 @@ function exponentialEaseOut(t: number): number {
|
|||||||
function gaplessHandler(args: {
|
function gaplessHandler(args: {
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
hasNextSong: boolean;
|
||||||
isFlac: boolean;
|
isFlac: boolean;
|
||||||
isTransitioning: boolean | string;
|
isTransitioning: boolean | string;
|
||||||
nextPlayer: {
|
nextPlayer: {
|
||||||
@@ -688,7 +691,19 @@ function gaplessHandler(args: {
|
|||||||
};
|
};
|
||||||
setIsTransitioning: Dispatch<boolean | string>;
|
setIsTransitioning: Dispatch<boolean | string>;
|
||||||
}) {
|
}) {
|
||||||
const { currentTime, duration, isFlac, isTransitioning, nextPlayer, setIsTransitioning } = args;
|
const {
|
||||||
|
currentTime,
|
||||||
|
duration,
|
||||||
|
hasNextSong,
|
||||||
|
isFlac,
|
||||||
|
isTransitioning,
|
||||||
|
nextPlayer,
|
||||||
|
setIsTransitioning,
|
||||||
|
} = args;
|
||||||
|
|
||||||
|
if (!hasNextSong) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isTransitioning) {
|
if (!isTransitioning) {
|
||||||
if (currentTime > duration - 2) {
|
if (currentTime > duration - 2) {
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import type { QueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { autoDjGenreIdsForSongGenre, autoDjPushUniqueAlbumIds } from './auto-dj-utils';
|
||||||
|
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||||
|
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||||
|
import { AUTO_DJ_STRATEGY, type AutoDJStrategy } from '/@/renderer/store/settings.store';
|
||||||
|
import { shuffle } from '/@/renderer/utils/shuffle';
|
||||||
|
import {
|
||||||
|
AlbumListSort,
|
||||||
|
type QueueSong,
|
||||||
|
type ServerListItem,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export type AutoDjAlbumCollectArgs = {
|
||||||
|
albumStrategy: AutoDJStrategy;
|
||||||
|
currentSong: QueueSong;
|
||||||
|
itemCount: number;
|
||||||
|
musicFolderId: string | string[] | undefined;
|
||||||
|
queryClient: QueryClient;
|
||||||
|
queueAlbumIdSet: Set<string>;
|
||||||
|
server: null | ServerListItem | undefined;
|
||||||
|
serverId: string;
|
||||||
|
trySimilarSongs: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runAutoDjAlbumIds = async (args: AutoDjAlbumCollectArgs): Promise<string[]> => {
|
||||||
|
switch (args.albumStrategy) {
|
||||||
|
case AUTO_DJ_STRATEGY.LIBRARY_RANDOM: {
|
||||||
|
return collectAlbumsLibraryRandom(args);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return collectAlbumsSimilar(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectAlbumsLibraryRandom = async (args: AutoDjAlbumCollectArgs): Promise<string[]> => {
|
||||||
|
const page = await args.queryClient.fetchQuery({
|
||||||
|
...albumQueries.list({
|
||||||
|
query: {
|
||||||
|
limit: Math.max(args.itemCount, 1),
|
||||||
|
musicFolderId: args.musicFolderId,
|
||||||
|
sortBy: AlbumListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({ autoDjAlbumLibraryRandom: args.currentSong?.id }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ids = page.items.map((a) => a.id).filter((id) => id && !args.queueAlbumIdSet.has(id));
|
||||||
|
return shuffle(ids).slice(0, args.itemCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectAlbumsSimilar = async (args: AutoDjAlbumCollectArgs): Promise<string[]> => {
|
||||||
|
const targetAlbumCount = args.itemCount;
|
||||||
|
const candidateAlbumIds: string[] = [];
|
||||||
|
const seenAlbumCandidates = new Set<string>();
|
||||||
|
|
||||||
|
if (args.trySimilarSongs && args.currentSong?.id) {
|
||||||
|
const similarSongsFromSimilarApi = await args.queryClient.fetchQuery({
|
||||||
|
...songsQueries.similar({
|
||||||
|
query: {
|
||||||
|
count: args.itemCount * 4,
|
||||||
|
songId: args.currentSong.id,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
similarSongAlbumDj: args.currentSong.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
autoDjPushUniqueAlbumIds(
|
||||||
|
candidateAlbumIds,
|
||||||
|
seenAlbumCandidates,
|
||||||
|
args.queueAlbumIdSet,
|
||||||
|
...similarSongsFromSimilarApi.map((s) => s.albumId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateAlbumIds.length < targetAlbumCount && args.currentSong && args.server) {
|
||||||
|
const genre = args.currentSong.genres?.[0];
|
||||||
|
if (genre) {
|
||||||
|
const genreIds = autoDjGenreIdsForSongGenre(genre, args.server.type);
|
||||||
|
|
||||||
|
const genreAlbums = await args.queryClient.fetchQuery({
|
||||||
|
...albumQueries.list({
|
||||||
|
query: {
|
||||||
|
genreIds,
|
||||||
|
limit: 50,
|
||||||
|
musicFolderId: args.musicFolderId,
|
||||||
|
sortBy: AlbumListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
genreAlbumDj: genreIds,
|
||||||
|
song: args.currentSong.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
autoDjPushUniqueAlbumIds(
|
||||||
|
candidateAlbumIds,
|
||||||
|
seenAlbumCandidates,
|
||||||
|
args.queueAlbumIdSet,
|
||||||
|
...genreAlbums.items.map((album) => album.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!args.trySimilarSongs) {
|
||||||
|
const randomAlbumMixCount = Math.max(1, Math.ceil(50 * 0.2));
|
||||||
|
const randomAlbumsMix = await args.queryClient.fetchQuery({
|
||||||
|
...albumQueries.list({
|
||||||
|
query: {
|
||||||
|
limit: randomAlbumMixCount,
|
||||||
|
musicFolderId: args.musicFolderId,
|
||||||
|
sortBy: AlbumListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
genreAlbumDjMixRandom: args.currentSong.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
autoDjPushUniqueAlbumIds(
|
||||||
|
candidateAlbumIds,
|
||||||
|
seenAlbumCandidates,
|
||||||
|
args.queueAlbumIdSet,
|
||||||
|
...randomAlbumsMix.items.map((album) => album.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateAlbumIds.length < targetAlbumCount && args.currentSong) {
|
||||||
|
const albumArtist = args.currentSong.albumArtists?.[0];
|
||||||
|
|
||||||
|
if (albumArtist) {
|
||||||
|
const albumsByArtist = await args.queryClient.fetchQuery({
|
||||||
|
...albumQueries.list({
|
||||||
|
query: {
|
||||||
|
artistIds: [albumArtist.id],
|
||||||
|
limit: 50,
|
||||||
|
musicFolderId: args.musicFolderId,
|
||||||
|
sortBy: AlbumListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
artistAlbumDj: albumArtist.id,
|
||||||
|
song: args.currentSong.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
autoDjPushUniqueAlbumIds(
|
||||||
|
candidateAlbumIds,
|
||||||
|
seenAlbumCandidates,
|
||||||
|
args.queueAlbumIdSet,
|
||||||
|
...albumsByArtist.items.map((album) => album.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateAlbumIds.length < targetAlbumCount && args.currentSong) {
|
||||||
|
const randomAlbumsFallback = await args.queryClient.fetchQuery({
|
||||||
|
...albumQueries.list({
|
||||||
|
query: {
|
||||||
|
limit: 80,
|
||||||
|
musicFolderId: args.musicFolderId,
|
||||||
|
sortBy: AlbumListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
fallbackAlbumDj: args.currentSong.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
autoDjPushUniqueAlbumIds(
|
||||||
|
candidateAlbumIds,
|
||||||
|
seenAlbumCandidates,
|
||||||
|
args.queueAlbumIdSet,
|
||||||
|
...randomAlbumsFallback.items.map((album) => album.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shuffledAlbums = shuffle(candidateAlbumIds);
|
||||||
|
return shuffledAlbums.slice(0, targetAlbumCount);
|
||||||
|
};
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import type { QueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||||
|
import { AUTO_DJ_STRATEGY, type AutoDJStrategy } from '/@/renderer/store/settings.store';
|
||||||
|
import { shuffleInPlace } from '/@/renderer/utils/shuffle';
|
||||||
|
import {
|
||||||
|
Played,
|
||||||
|
type QueueSong,
|
||||||
|
type ServerListItem,
|
||||||
|
Song,
|
||||||
|
SongListSort,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export type AutoDjSongCollectArgs = {
|
||||||
|
currentSong: QueueSong;
|
||||||
|
itemCount: number;
|
||||||
|
musicFolderId: string | string[] | undefined;
|
||||||
|
queryClient: QueryClient;
|
||||||
|
queueSongIdSet: Set<string>;
|
||||||
|
server: null | ServerListItem | undefined;
|
||||||
|
serverId: string;
|
||||||
|
songStrategy: AutoDJStrategy;
|
||||||
|
trySimilarSongs: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runAutoDjSongs = async (args: AutoDjSongCollectArgs): Promise<Song[]> => {
|
||||||
|
switch (args.songStrategy) {
|
||||||
|
case AUTO_DJ_STRATEGY.LIBRARY_RANDOM: {
|
||||||
|
return collectSongsLibraryRandom(args);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return collectSongsSimilar(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectSongsLibraryRandom = async (args: AutoDjSongCollectArgs): Promise<Song[]> => {
|
||||||
|
const randomSongs = await args.queryClient.fetchQuery({
|
||||||
|
...songsQueries.random({
|
||||||
|
query: {
|
||||||
|
limit: Math.max(args.itemCount * 3, 50),
|
||||||
|
played: Played.All,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({ autoDjLibraryRandomSongs: args.currentSong.id }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const pool = randomSongs.items.filter((song) => !args.queueSongIdSet.has(song.id));
|
||||||
|
const shuffled = shuffleInPlace(pool);
|
||||||
|
return shuffled.slice(0, args.itemCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectSongsSimilar = async (args: AutoDjSongCollectArgs): Promise<Song[]> => {
|
||||||
|
let uniqueSimilarSongs: Song[] = [];
|
||||||
|
|
||||||
|
if (args.trySimilarSongs) {
|
||||||
|
const similarSongs = await args.queryClient.fetchQuery({
|
||||||
|
...songsQueries.similar({
|
||||||
|
query: {
|
||||||
|
count: args.itemCount,
|
||||||
|
songId: args.currentSong?.id,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({ similarSongs: args.currentSong?.id }),
|
||||||
|
});
|
||||||
|
|
||||||
|
uniqueSimilarSongs = similarSongs.filter((song) => !args.queueSongIdSet.has(song.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniqueSimilarSongs.length < args.itemCount) {
|
||||||
|
const genre = args.currentSong?.genres?.[0];
|
||||||
|
|
||||||
|
if (genre) {
|
||||||
|
const genreLimit = 50;
|
||||||
|
const genreSimilarSongs = await args.queryClient.fetchQuery({
|
||||||
|
...songsQueries.random({
|
||||||
|
query: {
|
||||||
|
genre: genre.id,
|
||||||
|
limit: genreLimit,
|
||||||
|
played: Played.All,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
genre,
|
||||||
|
similarSongs: args.currentSong?.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const genreSongs = genreSimilarSongs.items.filter(
|
||||||
|
(song) => !args.queueSongIdSet.has(song.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!args.trySimilarSongs) {
|
||||||
|
const randomSongCount = Math.max(1, Math.ceil(genreLimit * 0.2));
|
||||||
|
|
||||||
|
const randomSongs = await args.queryClient.fetchQuery({
|
||||||
|
...songsQueries.random({
|
||||||
|
query: { limit: randomSongCount, played: Played.All },
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const uniqueRandomSongs = randomSongs.items.filter(
|
||||||
|
(song) => !args.queueSongIdSet.has(song.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const randomSongsToAdd = uniqueRandomSongs.slice(0, randomSongCount);
|
||||||
|
uniqueSimilarSongs.push(...randomSongsToAdd, ...genreSongs);
|
||||||
|
} else {
|
||||||
|
uniqueSimilarSongs.push(...genreSongs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniqueSimilarSongs.length < args.itemCount) {
|
||||||
|
const albumArtist = args.currentSong?.albumArtists?.[0];
|
||||||
|
|
||||||
|
if (albumArtist) {
|
||||||
|
const albumArtistSimilarSongs = await args.queryClient.fetchQuery({
|
||||||
|
...songsQueries.list({
|
||||||
|
query: {
|
||||||
|
albumArtistIds: [albumArtist.id],
|
||||||
|
limit: 50,
|
||||||
|
sortBy: SongListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
queryKey: queryKeys.player.fetch({
|
||||||
|
albumArtist,
|
||||||
|
similarSongs: args.currentSong?.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
uniqueSimilarSongs.push(
|
||||||
|
...albumArtistSimilarSongs.items.filter(
|
||||||
|
(song) => !args.queueSongIdSet.has(song.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniqueSimilarSongs.length < args.itemCount) {
|
||||||
|
const randomSongs = await args.queryClient.fetchQuery({
|
||||||
|
...songsQueries.random({
|
||||||
|
query: { limit: 50, played: Played.All },
|
||||||
|
serverId: args.serverId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
uniqueSimilarSongs.push(
|
||||||
|
...randomSongs.items.filter((song) => !args.queueSongIdSet.has(song.id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shuffledSongs = shuffleInPlace(uniqueSimilarSongs);
|
||||||
|
return shuffledSongs.slice(0, args.itemCount);
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Genre } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
import { ServerType } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const autoDjPushUniqueAlbumIds = (
|
||||||
|
accumulator: string[],
|
||||||
|
seenAlbums: Set<string>,
|
||||||
|
queueAlbumIdSet: Set<string>,
|
||||||
|
...ids: (string | undefined)[]
|
||||||
|
) => {
|
||||||
|
for (const id of ids) {
|
||||||
|
if (!id || queueAlbumIdSet.has(id) || seenAlbums.has(id)) continue;
|
||||||
|
seenAlbums.add(id);
|
||||||
|
accumulator.push(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const autoDjGenreIdsForSongGenre = (genre: Genre, serverType: ServerType): string[] => {
|
||||||
|
if (serverType === ServerType.JELLYFIN) {
|
||||||
|
return [genre.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverType === ServerType.NAVIDROME || serverType === ServerType.SUBSONIC) {
|
||||||
|
return [genre.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [genre.id];
|
||||||
|
};
|
||||||
@@ -9,7 +9,13 @@ import styles from './playerbar-waveform.module.css';
|
|||||||
import { useSongUrl } from '/@/renderer/features/player/audio-player/hooks/use-stream-url';
|
import { useSongUrl } from '/@/renderer/features/player/audio-player/hooks/use-stream-url';
|
||||||
import { PlayerbarSeekSlider } from '/@/renderer/features/player/components/playerbar-seek-slider';
|
import { PlayerbarSeekSlider } from '/@/renderer/features/player/components/playerbar-seek-slider';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import { BarAlign, usePlayerbarSlider, usePlayerSong, usePlayerTimestamp } from '/@/renderer/store';
|
import {
|
||||||
|
BarAlign,
|
||||||
|
usePlaybackSettings,
|
||||||
|
usePlayerbarSlider,
|
||||||
|
usePlayerSong,
|
||||||
|
usePlayerTimestamp,
|
||||||
|
} from '/@/renderer/store';
|
||||||
import { useAppThemeColors, useColorScheme } from '/@/renderer/themes/use-app-theme';
|
import { useAppThemeColors, useColorScheme } from '/@/renderer/themes/use-app-theme';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
|
||||||
@@ -30,7 +36,12 @@ export const PlayerbarWaveform = () => {
|
|||||||
|
|
||||||
const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0;
|
const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0;
|
||||||
|
|
||||||
const streamUrl = useSongUrl(currentSong, true, { bitrate: 64, enabled: false, format: 'mp3' });
|
const { transcode } = usePlaybackSettings();
|
||||||
|
const streamUrl = useSongUrl(currentSong, true, {
|
||||||
|
bitrate: 64,
|
||||||
|
enabled: transcode.enabled,
|
||||||
|
format: 'mp3',
|
||||||
|
});
|
||||||
|
|
||||||
const { color } = useAppThemeColors();
|
const { color } = useAppThemeColors();
|
||||||
const primaryColor = (color['--theme-colors-primary'] as string) || 'rgb(53, 116, 252)';
|
const primaryColor = (color['--theme-colors-primary'] as string) || 'rgb(53, 116, 252)';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { useCallback, useEffect, useState, WheelEvent } from 'react';
|
import { useCallback, useEffect, useMemo, useState, WheelEvent } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { PopoverPlayQueue } from '/@/renderer/features/now-playing/components/popover-play-queue';
|
import { PopoverPlayQueue } from '/@/renderer/features/now-playing/components/popover-play-queue';
|
||||||
@@ -12,6 +12,9 @@ import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-
|
|||||||
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||||
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
||||||
import {
|
import {
|
||||||
|
AUTO_DJ_MODE,
|
||||||
|
AUTO_DJ_STRATEGY,
|
||||||
|
type AutoDJStrategy,
|
||||||
useAppStoreActions,
|
useAppStoreActions,
|
||||||
useAutoDJSettings,
|
useAutoDJSettings,
|
||||||
useCurrentServer,
|
useCurrentServer,
|
||||||
@@ -34,7 +37,15 @@ import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
|||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
|
import { Paper } from '/@/shared/components/paper/paper';
|
||||||
|
import { Popover } from '/@/shared/components/popover/popover';
|
||||||
import { Rating } from '/@/shared/components/rating/rating';
|
import { Rating } from '/@/shared/components/rating/rating';
|
||||||
|
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||||
|
import { Select } from '/@/shared/components/select/select';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
|
import { Switch } from '/@/shared/components/switch/switch';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { useMediaQuery } from '/@/shared/hooks/use-media-query';
|
import { useMediaQuery } from '/@/shared/hooks/use-media-query';
|
||||||
import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback';
|
import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback';
|
||||||
import { useThrottledValue } from '/@/shared/hooks/use-throttled-value';
|
import { useThrottledValue } from '/@/shared/hooks/use-throttled-value';
|
||||||
@@ -90,20 +101,49 @@ const AutoDJButton = () => {
|
|||||||
const settings = useAutoDJSettings();
|
const settings = useAutoDJSettings();
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
const toggleAutoDJ = () => {
|
const itemLabels = useMemo(() => {
|
||||||
setSettings({
|
return {
|
||||||
autoDJ: {
|
description: t('setting.autoDJ_itemCount_description'),
|
||||||
...settings,
|
title: t('setting.autoDJ_itemCount'),
|
||||||
enabled: !settings.enabled,
|
};
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
const strategySelectData = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: t('setting.autoDJ_strategy_option_similar'),
|
||||||
|
value: AUTO_DJ_STRATEGY.SIMILAR,
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
label: t('setting.autoDJ_strategy_option_library_random'),
|
||||||
|
value: AUTO_DJ_STRATEGY.LIBRARY_RANDOM,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const strategyLabels =
|
||||||
|
settings.mode === AUTO_DJ_MODE.ALBUMS
|
||||||
|
? {
|
||||||
|
description: '',
|
||||||
|
title: t('setting.autoDJ_albumStrategy'),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
description: '',
|
||||||
|
title: t('setting.autoDJ_songStrategy'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const strategyValue =
|
||||||
|
settings.mode === AUTO_DJ_MODE.ALBUMS
|
||||||
|
? (settings.albumStrategy ?? AUTO_DJ_STRATEGY.SIMILAR)
|
||||||
|
: (settings.songStrategy ?? AUTO_DJ_STRATEGY.SIMILAR);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Popover position="top-end" withArrow>
|
||||||
|
<Popover.Target>
|
||||||
<Button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleAutoDJ();
|
|
||||||
}}
|
}}
|
||||||
size="compact-xs"
|
size="compact-xs"
|
||||||
style={{ color: settings.enabled ? 'var(--theme-colors-primary)' : undefined }}
|
style={{ color: settings.enabled ? 'var(--theme-colors-primary)' : undefined }}
|
||||||
@@ -112,6 +152,97 @@ const AutoDJButton = () => {
|
|||||||
>
|
>
|
||||||
{t('setting.autoDJ')}
|
{t('setting.autoDJ')}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown maw={320} miw={260} onClick={(e) => e.stopPropagation()} p="sm">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Paper p="md" radius="md">
|
||||||
|
<Group align="center" gap="xs" justify="space-between" wrap="nowrap">
|
||||||
|
<Text fw={600} isNoSelect size="sm">
|
||||||
|
{t('setting.autoDJ_enabled')}
|
||||||
|
</Text>
|
||||||
|
<Switch
|
||||||
|
checked={settings.enabled}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({
|
||||||
|
autoDJ: { enabled: e.currentTarget.checked },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
<SegmentedControl
|
||||||
|
data={[
|
||||||
|
{ label: t('setting.autoDJ_mode_songs'), value: AUTO_DJ_MODE.SONGS },
|
||||||
|
{
|
||||||
|
label: t('setting.autoDJ_mode_albums'),
|
||||||
|
value: AUTO_DJ_MODE.ALBUMS,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) =>
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
mode: value as 'albums' | 'songs',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value={settings.mode}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
comboboxProps={{ withinPortal: false }}
|
||||||
|
data={strategySelectData}
|
||||||
|
description={strategyLabels.description}
|
||||||
|
label={strategyLabels.title}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!value) return;
|
||||||
|
setSettings({
|
||||||
|
autoDJ:
|
||||||
|
settings.mode === AUTO_DJ_MODE.ALBUMS
|
||||||
|
? { albumStrategy: value as AutoDJStrategy }
|
||||||
|
: { songStrategy: value as AutoDJStrategy },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="md"
|
||||||
|
value={strategyValue}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
aria-label={itemLabels.title}
|
||||||
|
description={itemLabels.description}
|
||||||
|
hideControls={false}
|
||||||
|
label={itemLabels.title}
|
||||||
|
max={50}
|
||||||
|
min={1}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
itemCount: Number(e),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size="md"
|
||||||
|
value={Number(settings.itemCount)}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
aria-label={t('setting.autoDJ_timing')}
|
||||||
|
description={t('setting.autoDJ_timing_description')}
|
||||||
|
hideControls={false}
|
||||||
|
label={t('setting.autoDJ_timing')}
|
||||||
|
max={5}
|
||||||
|
min={1}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
timing: Number(e),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size="md"
|
||||||
|
value={Number(settings.timing)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const ScrobbleStatus = ({ formattedTime }: { formattedTime: string }) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HoverCard position="top" width={280}>
|
<HoverCard openDelay={500} position="top" width={280}>
|
||||||
<HoverCard.Target>
|
<HoverCard.Target>
|
||||||
<Group
|
<Group
|
||||||
align="center"
|
align="center"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { createWithEqualityFn } from 'zustand/traditional';
|
|||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||||
import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
|
import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import { PlayButtonGroup } from '/@/renderer/features/shared/components/play-button-group';
|
import { PlayButtonGroup } from '/@/renderer/features/shared/components/play-button-group';
|
||||||
@@ -18,9 +19,18 @@ import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
|||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
|
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||||
import { Select } from '/@/shared/components/select/select';
|
import { Select } from '/@/shared/components/select/select';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Played, RandomSongListQuery, ServerType } from '/@/shared/types/domain-types';
|
import {
|
||||||
|
AlbumListQuery,
|
||||||
|
AlbumListSort,
|
||||||
|
LibraryItem,
|
||||||
|
Played,
|
||||||
|
RandomSongListQuery,
|
||||||
|
ServerType,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface ShuffleAllSlice extends RandomSongListQuery {
|
interface ShuffleAllSlice extends RandomSongListQuery {
|
||||||
@@ -29,6 +39,7 @@ interface ShuffleAllSlice extends RandomSongListQuery {
|
|||||||
};
|
};
|
||||||
enableMaxYear: boolean;
|
enableMaxYear: boolean;
|
||||||
enableMinYear: boolean;
|
enableMinYear: boolean;
|
||||||
|
playbackKind: 'albums' | 'songs';
|
||||||
}
|
}
|
||||||
|
|
||||||
const useShuffleAllStore = createWithEqualityFn<ShuffleAllSlice>()(
|
const useShuffleAllStore = createWithEqualityFn<ShuffleAllSlice>()(
|
||||||
@@ -42,16 +53,28 @@ const useShuffleAllStore = createWithEqualityFn<ShuffleAllSlice>()(
|
|||||||
enableMaxYear: false,
|
enableMaxYear: false,
|
||||||
enableMinYear: false,
|
enableMinYear: false,
|
||||||
genre: '',
|
genre: '',
|
||||||
|
limit: 100,
|
||||||
maxYear: 2020,
|
maxYear: 2020,
|
||||||
minYear: 2000,
|
minYear: 2000,
|
||||||
musicFolder: '',
|
musicFolder: '',
|
||||||
|
playbackKind: 'songs',
|
||||||
played: Played.All,
|
played: Played.All,
|
||||||
songCount: 100,
|
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
merge: (persistedState, currentState) => merge(currentState, persistedState),
|
merge: (persistedState, currentState) => merge(currentState, persistedState),
|
||||||
|
migrate: (persisted, version: number) => {
|
||||||
|
if (!persisted) {
|
||||||
|
return persisted;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version >= 2) {
|
||||||
|
return persisted;
|
||||||
|
}
|
||||||
|
|
||||||
|
return persisted;
|
||||||
|
},
|
||||||
name: 'store_shuffle_all',
|
name: 'store_shuffle_all',
|
||||||
version: 1,
|
version: 2,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -66,13 +89,24 @@ export const useShuffleAllStoreActions = () => useShuffleAllStore((state) => sta
|
|||||||
|
|
||||||
export const ShuffleAllContextModal = () => {
|
export const ShuffleAllContextModal = () => {
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { addToQueueByData } = usePlayer();
|
const { addToQueueByData, addToQueueByFetch } = usePlayer();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { enableMaxYear, enableMinYear, genre, limit, maxYear, minYear, musicFolderId, played } =
|
const {
|
||||||
useShuffleAllStore();
|
enableMaxYear,
|
||||||
|
enableMinYear,
|
||||||
|
genre,
|
||||||
|
limit,
|
||||||
|
maxYear,
|
||||||
|
minYear,
|
||||||
|
musicFolderId,
|
||||||
|
playbackKind,
|
||||||
|
played,
|
||||||
|
} = useShuffleAllStore();
|
||||||
const { setStore } = useShuffleAllStoreActions();
|
const { setStore } = useShuffleAllStoreActions();
|
||||||
|
|
||||||
const { isFetching, refetch } = useQuery({
|
const clampedLimit = Math.min(500, Math.max(1, limit || 100));
|
||||||
|
|
||||||
|
const { isFetching: isFetchingSongs, refetch: refetchSongs } = useQuery({
|
||||||
...randomFetchQuery({
|
...randomFetchQuery({
|
||||||
query: {
|
query: {
|
||||||
genre: genre || undefined,
|
genre: genre || undefined,
|
||||||
@@ -89,22 +123,75 @@ export const ShuffleAllContextModal = () => {
|
|||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { isFetching: isFetchingAlbums, refetch: refetchAlbums } = useQuery({
|
||||||
|
...shuffleAlbumListQuery({
|
||||||
|
query: {
|
||||||
|
genreIds: genre ? [genre] : undefined,
|
||||||
|
limit: clampedLimit,
|
||||||
|
minYear: enableMinYear ? minYear || undefined : undefined,
|
||||||
|
musicFolderId: musicFolderId || undefined,
|
||||||
|
sortBy: AlbumListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId: server.id,
|
||||||
|
}),
|
||||||
|
enabled: false,
|
||||||
|
gcTime: 0,
|
||||||
|
staleTime: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const fetchTypeRef = useRef<Play>(null);
|
const fetchTypeRef = useRef<Play>(null);
|
||||||
|
|
||||||
const handlePlay = async (playType: Play) => {
|
const handlePlay = async (playType: Play) => {
|
||||||
fetchTypeRef.current = playType;
|
fetchTypeRef.current = playType;
|
||||||
|
|
||||||
const { data } = await refetch();
|
if (playbackKind === 'albums') {
|
||||||
|
const { data } = await refetchAlbums();
|
||||||
|
|
||||||
|
addToQueueByFetch(
|
||||||
|
server.id,
|
||||||
|
data?.items.map((a) => a.id) ?? [],
|
||||||
|
LibraryItem.ALBUM,
|
||||||
|
playType,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const { data } = await refetchSongs();
|
||||||
|
|
||||||
addToQueueByData(data?.items || [], playType);
|
addToQueueByData(data?.items || [], playType);
|
||||||
|
}
|
||||||
|
|
||||||
closeAllModals();
|
closeAllModals();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
|
<SegmentedControl
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: t('form.shuffleAll.input_kind_songs'),
|
||||||
|
value: 'songs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('form.shuffleAll.input_kind_albums'),
|
||||||
|
value: 'albums',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) =>
|
||||||
|
setStore({
|
||||||
|
playbackKind: value as 'albums' | 'songs',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
value={playbackKind}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label={t('form.shuffleAll.input_limit')}
|
label={
|
||||||
|
playbackKind === 'albums'
|
||||||
|
? t('form.shuffleAll.input_limit_albums')
|
||||||
|
: t('form.shuffleAll.input_limit_songs')
|
||||||
|
}
|
||||||
max={500}
|
max={500}
|
||||||
min={1}
|
min={1}
|
||||||
onChange={(e) => setStore({ limit: e ? Number(e) : 500 })}
|
onChange={(e) => setStore({ limit: e ? Number(e) : 500 })}
|
||||||
@@ -127,6 +214,7 @@ export const ShuffleAllContextModal = () => {
|
|||||||
value={minYear}
|
value={minYear}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
|
disabled={playbackKind === 'albums'}
|
||||||
label={t('form.shuffleAll.input_maxYear')}
|
label={t('form.shuffleAll.input_maxYear')}
|
||||||
max={2050}
|
max={2050}
|
||||||
min={1850}
|
min={1850}
|
||||||
@@ -134,6 +222,7 @@ export const ShuffleAllContextModal = () => {
|
|||||||
rightSection={
|
rightSection={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={enableMaxYear}
|
checked={enableMaxYear}
|
||||||
|
disabled={playbackKind === 'albums'}
|
||||||
onChange={(e) => setStore({ enableMaxYear: e.currentTarget.checked })}
|
onChange={(e) => setStore({ enableMaxYear: e.currentTarget.checked })}
|
||||||
style={{ marginRight: '0.5rem' }}
|
style={{ marginRight: '0.5rem' }}
|
||||||
/>
|
/>
|
||||||
@@ -144,7 +233,7 @@ export const ShuffleAllContextModal = () => {
|
|||||||
<Suspense fallback={<Select data={[]} />}>
|
<Suspense fallback={<Select data={[]} />}>
|
||||||
<GenreSelect />
|
<GenreSelect />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
{server?.type === ServerType.JELLYFIN && (
|
{server?.type === ServerType.JELLYFIN && playbackKind === 'songs' && (
|
||||||
<Select
|
<Select
|
||||||
clearable
|
clearable
|
||||||
data={PLAYED_DATA}
|
data={PLAYED_DATA}
|
||||||
@@ -156,10 +245,7 @@ export const ShuffleAllContextModal = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
<PlayButtonGroup
|
<PlayButtonGroup loading={isFetchingSongs || isFetchingAlbums} onPlay={handlePlay} />
|
||||||
loading={(isFetching && fetchTypeRef.current) || false}
|
|
||||||
onPlay={handlePlay}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -186,6 +272,13 @@ const randomFetchQuery = (args: {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const shuffleAlbumListQuery = (args: { query: AlbumListQuery; serverId: string }) => {
|
||||||
|
return albumQueries.list({
|
||||||
|
query: args.query,
|
||||||
|
serverId: args.serverId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const openShuffleAllModal = async () => {
|
export const openShuffleAllModal = async () => {
|
||||||
openContextModal({
|
openContextModal({
|
||||||
innerProps: {},
|
innerProps: {},
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
|
||||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||||
|
import { runAutoDjAlbumIds } from '/@/renderer/features/player/auto-dj/auto-dj-albums';
|
||||||
|
import { runAutoDjSongs } from '/@/renderer/features/player/auto-dj/auto-dj-songs';
|
||||||
import { useIsPlayerFetching, usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { useIsPlayerFetching, usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
|
||||||
import {
|
import {
|
||||||
|
AUTO_DJ_STRATEGY,
|
||||||
isShuffleEnabled,
|
isShuffleEnabled,
|
||||||
mapShuffledToQueueIndex,
|
mapShuffledToQueueIndex,
|
||||||
useAutoDJSettings,
|
useAutoDJSettings,
|
||||||
@@ -17,9 +18,8 @@ import {
|
|||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||||
import { shuffleInPlace } from '/@/renderer/utils/shuffle';
|
|
||||||
import { hasFeature } from '/@/shared/api/utils';
|
import { hasFeature } from '/@/shared/api/utils';
|
||||||
import { Played, Song, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { ServerFeature } from '/@/shared/types/features-types';
|
import { ServerFeature } from '/@/shared/types/features-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -34,6 +34,9 @@ export const useAutoDJ = () => {
|
|||||||
const hasSimilarSongsMusicFolder = hasFeature(server, ServerFeature.SIMILAR_SONGS_MUSIC_FOLDER);
|
const hasSimilarSongsMusicFolder = hasFeature(server, ServerFeature.SIMILAR_SONGS_MUSIC_FOLDER);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const albumStrategy = settings.albumStrategy ?? AUTO_DJ_STRATEGY.SIMILAR;
|
||||||
|
const songStrategy = settings.songStrategy ?? AUTO_DJ_STRATEGY.SIMILAR;
|
||||||
|
|
||||||
const unsubscribe = usePlayerStoreBase.subscribe(
|
const unsubscribe = usePlayerStoreBase.subscribe(
|
||||||
(state) => {
|
(state) => {
|
||||||
const queue = state.getQueue();
|
const queue = state.getQueue();
|
||||||
@@ -54,7 +57,6 @@ export const useAutoDJ = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no current song, don't autoplay
|
|
||||||
if (!properties.song?.id) {
|
if (!properties.song?.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -70,142 +72,76 @@ export const useAutoDJ = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const queue = usePlayerStore.getState().getQueue();
|
const queue = usePlayerStore.getState().getQueue();
|
||||||
const queueSongIdSet = new Set(queue.items.map((item) => item.id));
|
|
||||||
let uniqueSimilarSongs: Song[] = [];
|
|
||||||
|
|
||||||
const hasMusicFolder = server?.musicFolderId && server.musicFolderId.length > 0;
|
const hasMusicFolder = server?.musicFolderId && server.musicFolderId.length > 0;
|
||||||
|
const musicFolderId =
|
||||||
|
hasMusicFolder && server?.musicFolderId ? server.musicFolderId : undefined;
|
||||||
const trySimilarSongs =
|
const trySimilarSongs =
|
||||||
!hasMusicFolder || (hasMusicFolder && hasSimilarSongsMusicFolder);
|
!hasMusicFolder || (hasMusicFolder && hasSimilarSongsMusicFolder);
|
||||||
|
|
||||||
// Skip similar songs fetch if a music folder is selected and does not support musicFolderId on similar songs
|
const runnerDepsBase = {
|
||||||
if (trySimilarSongs) {
|
itemCount: settings.itemCount,
|
||||||
// First, try to fetch similar songs based on the current song
|
musicFolderId,
|
||||||
const similarSongs = await queryClient.fetchQuery({
|
queryClient,
|
||||||
...songsQueries.similar({
|
server,
|
||||||
query: {
|
|
||||||
count: settings.itemCount,
|
|
||||||
songId: properties.song?.id,
|
|
||||||
},
|
|
||||||
serverId,
|
serverId,
|
||||||
}),
|
trySimilarSongs,
|
||||||
queryKey: queryKeys.player.fetch({ similarSongs: properties.song?.id }),
|
};
|
||||||
|
|
||||||
|
if (settings.mode === 'albums') {
|
||||||
|
if (!serverId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queueAlbumIdSet = new Set(
|
||||||
|
queue.items
|
||||||
|
.map((item) => item.albumId)
|
||||||
|
.filter((id): id is string => Boolean(id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const albumsToAdd = await runAutoDjAlbumIds({
|
||||||
|
...runnerDepsBase,
|
||||||
|
albumStrategy,
|
||||||
|
currentSong: properties.song,
|
||||||
|
queueAlbumIdSet,
|
||||||
});
|
});
|
||||||
|
|
||||||
uniqueSimilarSongs = similarSongs.filter(
|
if (albumsToAdd.length > 0) {
|
||||||
(song) => !queueSongIdSet.has(song.id),
|
await player.addToQueueByFetch(
|
||||||
|
serverId,
|
||||||
|
albumsToAdd,
|
||||||
|
LibraryItem.ALBUM,
|
||||||
|
Play.LAST,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
eventEmitter.emit('AUTODJ_QUEUE_ADDED', {
|
||||||
|
songCount: albumsToAdd.length,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not enough songs, try to fetch more similar songs based on the genre of the current song
|
return;
|
||||||
if (uniqueSimilarSongs.length < settings.itemCount) {
|
}
|
||||||
const genre = properties.song?.genres?.[0];
|
|
||||||
|
|
||||||
if (genre) {
|
if (!serverId) {
|
||||||
const genreLimit = 50;
|
return;
|
||||||
const genreSimilarSongs = await queryClient.fetchQuery({
|
}
|
||||||
...songsQueries.random({
|
|
||||||
query: {
|
const queueSongIdSet = new Set(queue.items.map((item) => item.id));
|
||||||
genre: genre.id,
|
|
||||||
limit: genreLimit,
|
const songsToAdd = await runAutoDjSongs({
|
||||||
played: Played.All,
|
...runnerDepsBase,
|
||||||
},
|
currentSong: properties.song,
|
||||||
serverId,
|
queueSongIdSet,
|
||||||
}),
|
songStrategy,
|
||||||
queryKey: queryKeys.player.fetch({
|
|
||||||
genre,
|
|
||||||
similarSongs: properties.song?.id,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const genreSongs = genreSimilarSongs.items.filter(
|
if (songsToAdd.length > 0) {
|
||||||
(song) => !queueSongIdSet.has(song.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
// If trySimilarSongs is false, add variation by mixing in random songs
|
|
||||||
if (!trySimilarSongs) {
|
|
||||||
// Calculate how many random songs we need: 20% or at least 1
|
|
||||||
const randomSongCount = Math.max(1, Math.ceil(genreLimit * 0.2));
|
|
||||||
|
|
||||||
const randomSongs = await queryClient.fetchQuery({
|
|
||||||
...songsQueries.random({
|
|
||||||
query: { limit: randomSongCount, played: Played.All },
|
|
||||||
serverId,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const uniqueRandomSongs = randomSongs.items.filter(
|
|
||||||
(song) => !queueSongIdSet.has(song.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add minimum required random songs for variation
|
|
||||||
const randomSongsToAdd = uniqueRandomSongs.slice(
|
|
||||||
0,
|
|
||||||
randomSongCount,
|
|
||||||
);
|
|
||||||
uniqueSimilarSongs.push(...randomSongsToAdd, ...genreSongs);
|
|
||||||
} else {
|
|
||||||
uniqueSimilarSongs.push(...genreSongs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not enough songs, try to fetch more similar songs based on the album artist of the current song
|
|
||||||
if (uniqueSimilarSongs.length < settings.itemCount) {
|
|
||||||
const albumArtist = properties.song?.albumArtists?.[0];
|
|
||||||
|
|
||||||
if (albumArtist) {
|
|
||||||
const albumArtistSimilarSongs = await queryClient.fetchQuery({
|
|
||||||
...songsQueries.list({
|
|
||||||
query: {
|
|
||||||
albumArtistIds: [albumArtist.id],
|
|
||||||
limit: 50,
|
|
||||||
sortBy: SongListSort.RANDOM,
|
|
||||||
sortOrder: SortOrder.ASC,
|
|
||||||
startIndex: 0,
|
|
||||||
},
|
|
||||||
serverId,
|
|
||||||
}),
|
|
||||||
queryKey: queryKeys.player.fetch({
|
|
||||||
albumArtist,
|
|
||||||
similarSongs: properties.song?.id,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
uniqueSimilarSongs.push(
|
|
||||||
...albumArtistSimilarSongs.items.filter(
|
|
||||||
(song) => !queueSongIdSet.has(song.id),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not enough songs, just fetch fully random songs
|
|
||||||
if (uniqueSimilarSongs.length < settings.itemCount) {
|
|
||||||
const randomSongs = await queryClient.fetchQuery({
|
|
||||||
...songsQueries.random({
|
|
||||||
query: { limit: 50, played: Played.All },
|
|
||||||
serverId,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
uniqueSimilarSongs.push(
|
|
||||||
...randomSongs.items.filter((song) => !queueSongIdSet.has(song.id)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shuffle the songs and then add to the queue
|
|
||||||
const shuffledSongs = shuffleInPlace(uniqueSimilarSongs);
|
|
||||||
|
|
||||||
// Splice the first itemCount songs and add to the queue
|
|
||||||
const songsToAdd = shuffledSongs.slice(0, settings.itemCount);
|
|
||||||
|
|
||||||
// Add to the end of the queue
|
|
||||||
player.addToQueueByData(songsToAdd, Play.LAST);
|
player.addToQueueByData(songsToAdd, Play.LAST);
|
||||||
|
|
||||||
// Emit event to trigger queue follow
|
|
||||||
eventEmitter.emit('AUTODJ_QUEUE_ADDED', {
|
eventEmitter.emit('AUTODJ_QUEUE_ADDED', {
|
||||||
songCount: songsToAdd.length,
|
songCount: songsToAdd.length,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logFn.error(logMsg[LogCategory.PLAYER].autoPlayFailed, {
|
logFn.error(logMsg[LogCategory.PLAYER].autoPlayFailed, {
|
||||||
category: LogCategory.PLAYER,
|
category: LogCategory.PLAYER,
|
||||||
@@ -229,7 +165,10 @@ export const useAutoDJ = () => {
|
|||||||
server,
|
server,
|
||||||
serverId,
|
serverId,
|
||||||
settings.enabled,
|
settings.enabled,
|
||||||
|
settings.albumStrategy,
|
||||||
settings.itemCount,
|
settings.itemCount,
|
||||||
|
settings.mode,
|
||||||
|
settings.songStrategy,
|
||||||
settings.timing,
|
settings.timing,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -117,11 +117,11 @@ export const useMPRIS = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mpris?.requestPosition((_e: unknown, data: { position: number }) => {
|
mpris?.requestPosition((data: { position: number }) => {
|
||||||
player.mediaSeekToTimestamp(data.position);
|
player.mediaSeekToTimestamp(data.position);
|
||||||
});
|
});
|
||||||
|
|
||||||
mpris?.requestSeek((_e: unknown, data: { offset: number }) => {
|
mpris?.requestSeek((data: { offset: number }) => {
|
||||||
player.mediaSkipForward(data.offset);
|
player.mediaSkipForward(data.offset);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ export const useMPRIS = () => {
|
|||||||
player.toggleShuffle();
|
player.toggleShuffle();
|
||||||
});
|
});
|
||||||
|
|
||||||
mpris?.requestVolume((_e: unknown, data: { volume: number }) => {
|
mpris?.requestVolume((data: { volume: number }) => {
|
||||||
player.setVolume(data.volume);
|
player.setVolume(data.volume);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,27 +4,27 @@ import React, { useCallback, useEffect } from 'react';
|
|||||||
import { usePlayerStatus, useSettingsStore, useWindowSettings } from '/@/renderer/store';
|
import { usePlayerStatus, useSettingsStore, useWindowSettings } from '/@/renderer/store';
|
||||||
import { PlayerStatus } from '/@/shared/types/types';
|
import { PlayerStatus } from '/@/shared/types/types';
|
||||||
|
|
||||||
const ipc = isElectron() ? window.api.ipc : null;
|
const utils = isElectron() ? window.api.utils : null;
|
||||||
|
|
||||||
export const usePowerSaveBlocker = () => {
|
export const usePowerSaveBlocker = () => {
|
||||||
const status = usePlayerStatus();
|
const status = usePlayerStatus();
|
||||||
const { preventSleepOnPlayback, preventSuspendOnPlayback } = useWindowSettings();
|
const { preventSleepOnPlayback, preventSuspendOnPlayback } = useWindowSettings();
|
||||||
|
|
||||||
const startPowerSaveBlocker = useCallback(async () => {
|
const startPowerSaveBlocker = useCallback(async () => {
|
||||||
if (!ipc) return;
|
if (!utils) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ipc.invoke('power-save-blocker-start', { full: preventSleepOnPlayback });
|
await utils.startPowerSaveBlocker(preventSleepOnPlayback);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to start power save blocker:', error);
|
console.error('Failed to start power save blocker:', error);
|
||||||
}
|
}
|
||||||
}, [preventSleepOnPlayback]);
|
}, [preventSleepOnPlayback]);
|
||||||
|
|
||||||
const stopPowerSaveBlocker = useCallback(async () => {
|
const stopPowerSaveBlocker = useCallback(async () => {
|
||||||
if (!ipc) return;
|
if (!utils) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ipc.invoke('power-save-blocker-stop');
|
await utils.stopPowerSaveBlocker();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to stop power save blocker:', error);
|
console.error('Failed to stop power save blocker:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,9 +180,6 @@ export const useScrobble = () => {
|
|||||||
|
|
||||||
const currentSong = usePlayerStore.getState().getCurrentSong();
|
const currentSong = usePlayerStore.getState().getCurrentSong();
|
||||||
const mediaType = currentSong?._itemType.includes('song') ? 'song' : 'podcast';
|
const mediaType = currentSong?._itemType.includes('song') ? 'song' : 'podcast';
|
||||||
const serverId = currentSong?._serverId;
|
|
||||||
const server = getServerById(serverId);
|
|
||||||
const hasPlaybackReport = hasFeature(server, ServerFeature.REPORT_PLAYBACK);
|
|
||||||
const useTicks = currentSong?._serverType === ServerType.JELLYFIN;
|
const useTicks = currentSong?._serverType === ServerType.JELLYFIN;
|
||||||
const currentStatus = usePlayerStore.getState().player.status;
|
const currentStatus = usePlayerStore.getState().player.status;
|
||||||
const currentTime = properties.timestamp;
|
const currentTime = properties.timestamp;
|
||||||
@@ -239,36 +236,36 @@ export const useScrobble = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send progress events every 10 seconds
|
// Send progress events every 10 seconds
|
||||||
if (hasPlaybackReport) {
|
// if (hasPlaybackReport) {
|
||||||
const timeSinceLastProgress = currentTime - lastProgressEventRef.current;
|
// const timeSinceLastProgress = currentTime - lastProgressEventRef.current;
|
||||||
if (timeSinceLastProgress >= 10) {
|
// if (timeSinceLastProgress >= 10) {
|
||||||
sendScrobble.mutate(
|
// sendScrobble.mutate(
|
||||||
{
|
// {
|
||||||
apiClientProps: { serverId: serverId || '' },
|
// apiClientProps: { serverId: serverId || '' },
|
||||||
query: {
|
// query: {
|
||||||
albumId: currentSong.albumId,
|
// albumId: currentSong.albumId,
|
||||||
event: 'timeupdate',
|
// event: 'timeupdate',
|
||||||
id: currentSong.id,
|
// id: currentSong.id,
|
||||||
mediaType: mediaType,
|
// mediaType: mediaType,
|
||||||
playbackRate,
|
// playbackRate,
|
||||||
position: getPositionValue(currentTime, useTicks),
|
// position: getPositionValue(currentTime, useTicks),
|
||||||
submission: false,
|
// submission: false,
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
onSuccess: () => {
|
// onSuccess: () => {
|
||||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledTimeupdate, {
|
// logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledTimeupdate, {
|
||||||
category: LogCategory.SCROBBLE,
|
// category: LogCategory.SCROBBLE,
|
||||||
meta: {
|
// meta: {
|
||||||
id: currentSong.id,
|
// id: currentSong.id,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
lastProgressEventRef.current = currentTime;
|
// lastProgressEventRef.current = currentTime;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Check if we should submit scrobble based on listened time
|
// Check if we should submit scrobble based on listened time
|
||||||
if (!isCurrentSongScrobbledRef.current) {
|
if (!isCurrentSongScrobbledRef.current) {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export const PlaylistListInfiniteTable = ({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={loadedItems}
|
data={loadedItems}
|
||||||
enableAlternateRowColors={enableAlternateRowColors}
|
enableAlternateRowColors={enableAlternateRowColors}
|
||||||
|
enableExpansion={false}
|
||||||
enableHeader={enableHeader}
|
enableHeader={enableHeader}
|
||||||
enableHorizontalBorders={enableHorizontalBorders}
|
enableHorizontalBorders={enableHorizontalBorders}
|
||||||
enableRowHoverHighlight={enableRowHoverHighlight}
|
enableRowHoverHighlight={enableRowHoverHighlight}
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export const PlaylistListPaginatedTable = ({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={data || []}
|
data={data || []}
|
||||||
enableAlternateRowColors={enableAlternateRowColors}
|
enableAlternateRowColors={enableAlternateRowColors}
|
||||||
|
enableExpansion={false}
|
||||||
enableHeader={enableHeader}
|
enableHeader={enableHeader}
|
||||||
enableHorizontalBorders={enableHorizontalBorders}
|
enableHorizontalBorders={enableHorizontalBorders}
|
||||||
enableRowHoverHighlight={enableRowHoverHighlight}
|
enableRowHoverHighlight={enableRowHoverHighlight}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const useRemote = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
remote.requestPosition((_e: unknown, data: { position: number }) => {
|
remote.requestPosition((data: { position: number }) => {
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].requestPositionReceived, {
|
logFn.debug(logMsg[LogCategory.REMOTE].requestPositionReceived, {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: { position: data.position },
|
meta: { position: data.position },
|
||||||
@@ -73,7 +73,7 @@ export const useRemote = () => {
|
|||||||
player.mediaSeekToTimestamp(newTime);
|
player.mediaSeekToTimestamp(newTime);
|
||||||
});
|
});
|
||||||
|
|
||||||
remote.requestSeek((_e: unknown, data: { offset: number }) => {
|
remote.requestSeek((data: { offset: number }) => {
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].requestSeekReceived, {
|
logFn.debug(logMsg[LogCategory.REMOTE].requestSeekReceived, {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: { offset: data.offset },
|
meta: { offset: data.offset },
|
||||||
@@ -81,17 +81,15 @@ export const useRemote = () => {
|
|||||||
mediaSkipForward(data.offset);
|
mediaSkipForward(data.offset);
|
||||||
});
|
});
|
||||||
|
|
||||||
remote.requestRating(
|
remote.requestRating((data: { id: string; rating: number; serverId: string }) => {
|
||||||
(_e: unknown, data: { id: string; rating: number; serverId: string }) => {
|
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].requestRatingReceived, {
|
logFn.debug(logMsg[LogCategory.REMOTE].requestRatingReceived, {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: { id: data.id, rating: data.rating, serverId: data.serverId },
|
meta: { id: data.id, rating: data.rating, serverId: data.serverId },
|
||||||
});
|
});
|
||||||
setRating(data.serverId, [data.id], LibraryItem.SONG, data.rating);
|
setRating(data.serverId, [data.id], LibraryItem.SONG, data.rating);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
remote.requestVolume((_e: unknown, data: { volume: number }) => {
|
remote.requestVolume((data: { volume: number }) => {
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].requestVolumeReceived, {
|
logFn.debug(logMsg[LogCategory.REMOTE].requestVolumeReceived, {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: { volume: data.volume },
|
meta: { volume: data.volume },
|
||||||
@@ -99,15 +97,12 @@ export const useRemote = () => {
|
|||||||
setVolume(data.volume);
|
setVolume(data.volume);
|
||||||
});
|
});
|
||||||
|
|
||||||
remote.requestFavorite(
|
remote.requestFavorite((data: { favorite: boolean; id: string; serverId: string }) => {
|
||||||
(_e: unknown, data: { favorite: boolean; id: string; serverId: string }) => {
|
|
||||||
logFn.debug(logMsg[LogCategory.REMOTE].requestFavoriteReceived, {
|
logFn.debug(logMsg[LogCategory.REMOTE].requestFavoriteReceived, {
|
||||||
category: LogCategory.REMOTE,
|
category: LogCategory.REMOTE,
|
||||||
meta: { favorite: data.favorite, id: data.id, serverId: data.serverId },
|
meta: { favorite: data.favorite, id: data.id, serverId: data.serverId },
|
||||||
});
|
});
|
||||||
const mutator = data.favorite
|
const mutator = data.favorite ? addToFavoritesMutation : removeFromFavoritesMutation;
|
||||||
? addToFavoritesMutation
|
|
||||||
: removeFromFavoritesMutation;
|
|
||||||
mutator.mutate({
|
mutator.mutate({
|
||||||
apiClientProps: { serverId: data.serverId },
|
apiClientProps: { serverId: data.serverId },
|
||||||
query: {
|
query: {
|
||||||
@@ -115,8 +110,7 @@ export const useRemote = () => {
|
|||||||
type: LibraryItem.SONG,
|
type: LibraryItem.SONG,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ipc?.removeAllListeners('request-position');
|
ipc?.removeAllListeners('request-position');
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import isElectron from 'is-electron';
|
||||||
import { memo, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@@ -14,18 +15,39 @@ export const StylesSettings = memo(() => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const utils = isElectron() ? window.api.utils : null;
|
||||||
|
const isDesktop = isElectron();
|
||||||
|
|
||||||
const { content, enabled } = useCssSettings();
|
const { content, enabled } = useCssSettings();
|
||||||
const [css, setCss] = useState(content);
|
const [css, setCss] = useState(content);
|
||||||
|
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = async () => {
|
||||||
setSettings({
|
setSettings({
|
||||||
css: {
|
css: {
|
||||||
content: css,
|
content: css,
|
||||||
enabled,
|
enabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (utils) {
|
||||||
|
try {
|
||||||
|
await utils.saveCustomCss(css);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save custom css file', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenFolder = async () => {
|
||||||
|
if (!utils) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await utils.openCustomCssFolder();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open custom css folder', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -62,6 +84,15 @@ export const StylesSettings = memo(() => {
|
|||||||
<SettingsOptions
|
<SettingsOptions
|
||||||
control={
|
control={
|
||||||
<>
|
<>
|
||||||
|
{isDesktop && (
|
||||||
|
<Button
|
||||||
|
onClick={handleOpenFolder}
|
||||||
|
size="compact-md"
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
{t('common.openFolder', { postProcess: 'titleCase' })}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{open && (
|
{open && (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { IpcRendererEvent } from 'electron';
|
|
||||||
|
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
@@ -124,7 +122,7 @@ export const ApplicationSettings = memo(() => {
|
|||||||
// }, [fontSettings.custom]);
|
// }, [fontSettings.custom]);
|
||||||
|
|
||||||
const onFontError = useCallback(
|
const onFontError = useCallback(
|
||||||
(_: IpcRendererEvent, file: string) => {
|
(file: string) => {
|
||||||
toast.error({
|
toast.error({
|
||||||
message: `${file} is not a valid font file`,
|
message: `${file} is not a valid font file`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
|||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Table } from '/@/shared/components/table/table';
|
import { Table } from '/@/shared/components/table/table';
|
||||||
import { TextInput } from '/@/shared/components/text-input/text-input';
|
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||||
|
import {
|
||||||
|
keyboardCodeToHotkeyKey,
|
||||||
|
MODIFIER_KEY_CODES,
|
||||||
|
} from '/@/shared/utils/keyboard-code-to-hotkey';
|
||||||
|
|
||||||
const ipc = isElectron() ? window.api.ipc : null;
|
const ipc = isElectron() ? window.api.ipc : null;
|
||||||
|
|
||||||
@@ -112,25 +116,16 @@ export const HotkeyManagerSettings = memo(() => {
|
|||||||
const debouncedSetHotkey = debounce(
|
const debouncedSetHotkey = debounce(
|
||||||
(binding: BindingActions, e: KeyboardEvent<HTMLInputElement>) => {
|
(binding: BindingActions, e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const IGNORED_KEYS = ['Control', 'Alt', 'Shift', 'Meta', ' ', 'Escape'];
|
|
||||||
const keys: string[] = [];
|
const keys: string[] = [];
|
||||||
if (e.ctrlKey) keys.push('mod');
|
if (e.ctrlKey) keys.push('mod');
|
||||||
if (e.altKey) keys.push('alt');
|
if (e.altKey) keys.push('alt');
|
||||||
if (e.shiftKey) keys.push('shift');
|
if (e.shiftKey) keys.push('shift');
|
||||||
if (e.metaKey) keys.push('meta');
|
if (e.metaKey) keys.push('meta');
|
||||||
if (e.key === ' ') keys.push('space');
|
|
||||||
if (!IGNORED_KEYS.includes(e.key)) {
|
if (!MODIFIER_KEY_CODES.has(e.code) && e.code !== 'Escape') {
|
||||||
if (e.code.includes('Numpad')) {
|
const hotkeyKey = keyboardCodeToHotkeyKey(e.code);
|
||||||
if (e.key === '+') keys.push('numpadadd');
|
if (hotkeyKey) {
|
||||||
else if (e.key === '-') keys.push('numpadsubtract');
|
keys.push(hotkeyKey);
|
||||||
else if (e.key === '*') keys.push('numpadmultiply');
|
|
||||||
else if (e.key === '/') keys.push('numpaddivide');
|
|
||||||
else if (e.key === '.') keys.push('numpaddecimal');
|
|
||||||
else keys.push(`numpad${e.key}`.toLowerCase());
|
|
||||||
} else if (e.key === '+') {
|
|
||||||
keys.push('equal');
|
|
||||||
} else {
|
|
||||||
keys.push(e.key?.toLowerCase());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,112 @@
|
|||||||
import { memo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SettingOption,
|
SettingOption,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
} from '/@/renderer/features/settings/components/settings-section';
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
import { useAutoDJSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
import {
|
||||||
|
AUTO_DJ_MODE,
|
||||||
|
AUTO_DJ_STRATEGY,
|
||||||
|
type AutoDJStrategy,
|
||||||
|
useAutoDJSettings,
|
||||||
|
useSettingsStoreActions,
|
||||||
|
} from '/@/renderer/store/settings.store';
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
|
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||||
|
import { Select } from '/@/shared/components/select/select';
|
||||||
|
|
||||||
export const AutoDJSettings = memo(() => {
|
export const AutoDJSettings = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const settings = useAutoDJSettings();
|
const settings = useAutoDJSettings();
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
|
const itemLabels = useMemo(() => {
|
||||||
|
return {
|
||||||
|
description: t('setting.autoDJ_itemCount_description'),
|
||||||
|
title: t('setting.autoDJ_itemCount'),
|
||||||
|
};
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
const strategySelectData = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: t('setting.autoDJ_strategy_option_similar'),
|
||||||
|
value: AUTO_DJ_STRATEGY.SIMILAR,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('setting.autoDJ_strategy_option_library_random'),
|
||||||
|
value: AUTO_DJ_STRATEGY.LIBRARY_RANDOM,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
const autoDJOptions: SettingOption[] = [
|
const autoDJOptions: SettingOption[] = [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<SegmentedControl
|
||||||
|
data={[
|
||||||
|
{ label: t('setting.autoDJ_mode_songs'), value: AUTO_DJ_MODE.SONGS },
|
||||||
|
{ label: t('setting.autoDJ_mode_albums'), value: AUTO_DJ_MODE.ALBUMS },
|
||||||
|
]}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
mode: value as 'albums' | 'songs',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
value={settings.mode}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.autoDJ_mode_description'),
|
||||||
|
title: t('setting.autoDJ_mode'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={strategySelectData}
|
||||||
|
onChange={(value) =>
|
||||||
|
value &&
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
songStrategy: value as AutoDJStrategy,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value={settings.songStrategy ?? AUTO_DJ_STRATEGY.SIMILAR}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: '',
|
||||||
|
title: t('setting.autoDJ_songStrategy'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={strategySelectData}
|
||||||
|
onChange={(value) =>
|
||||||
|
value &&
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
albumStrategy: value as AutoDJStrategy,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value={settings.albumStrategy ?? AUTO_DJ_STRATEGY.SIMILAR}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: '',
|
||||||
|
title: t('setting.autoDJ_albumStrategy'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
aria-label="Auto DJ item count"
|
aria-label={itemLabels.title}
|
||||||
hideControls={false}
|
hideControls={false}
|
||||||
max={50}
|
max={50}
|
||||||
min={1}
|
min={1}
|
||||||
@@ -31,10 +120,8 @@ export const AutoDJSettings = memo(() => {
|
|||||||
value={Number(settings.itemCount)}
|
value={Number(settings.itemCount)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
description: t('setting.autoDJ_itemCount', {
|
description: itemLabels.description,
|
||||||
context: 'description',
|
title: itemLabels.title,
|
||||||
}),
|
|
||||||
title: t('setting.autoDJ_itemCount'),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
|
|||||||
@@ -36,13 +36,12 @@ export const WindowSettings = memo(() => {
|
|||||||
if (!e) return;
|
if (!e) return;
|
||||||
|
|
||||||
// Platform.LINUX is used as the native frame option regardless of the actual platform
|
// Platform.LINUX is used as the native frame option regardless of the actual platform
|
||||||
const hasFrame = localSettings?.get('window_has_frame') as
|
const previousWindowBarStyle = settings.windowBarStyle;
|
||||||
| boolean
|
const isSwitchingToNative =
|
||||||
| undefined;
|
previousWindowBarStyle !== Platform.LINUX && e === Platform.LINUX;
|
||||||
const isSwitchingToFrame = !hasFrame && e === Platform.LINUX;
|
const isSwitchingFromNative =
|
||||||
const isSwitchingToNoFrame = hasFrame && e !== Platform.LINUX;
|
previousWindowBarStyle === Platform.LINUX && e !== Platform.LINUX;
|
||||||
|
const requireRestart = isSwitchingToNative || isSwitchingFromNative;
|
||||||
const requireRestart = isSwitchingToFrame || isSwitchingToNoFrame;
|
|
||||||
|
|
||||||
if (requireRestart) {
|
if (requireRestart) {
|
||||||
openRestartRequiredToast();
|
openRestartRequiredToast();
|
||||||
|
|||||||
Vendored
+3
@@ -1,8 +1,11 @@
|
|||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
ANALYTICS_DISABLED?: boolean | string;
|
ANALYTICS_DISABLED?: boolean | string;
|
||||||
|
FS_AUTO_DJ_ALBUM_STRATEGY?: string;
|
||||||
FS_AUTO_DJ_ENABLED?: string;
|
FS_AUTO_DJ_ENABLED?: string;
|
||||||
FS_AUTO_DJ_ITEM_COUNT?: string;
|
FS_AUTO_DJ_ITEM_COUNT?: string;
|
||||||
|
FS_AUTO_DJ_MODE?: string;
|
||||||
|
FS_AUTO_DJ_SONG_STRATEGY?: string;
|
||||||
FS_AUTO_DJ_TIMING?: string;
|
FS_AUTO_DJ_TIMING?: string;
|
||||||
FS_CSS_CONTENT?: string;
|
FS_CSS_CONTENT?: string;
|
||||||
FS_CSS_ENABLED?: string;
|
FS_CSS_ENABLED?: string;
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import {
|
|||||||
type HotkeyItem as MantineHotkeyItem,
|
type HotkeyItem as MantineHotkeyItem,
|
||||||
useHotkeys as useMantineHotkeys,
|
useHotkeys as useMantineHotkeys,
|
||||||
} from '@mantine/hooks';
|
} from '@mantine/hooks';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { useAppStore } from '/@/renderer/store';
|
import { useAppStore } from '/@/renderer/store';
|
||||||
|
import { withPhysicalKeys } from '/@/shared/utils/hotkeys';
|
||||||
|
|
||||||
const EMPTY_HOTKEYS: MantineHotkeyItem[] = [];
|
const EMPTY_HOTKEYS: MantineHotkeyItem[] = [];
|
||||||
|
|
||||||
@@ -13,8 +15,10 @@ export const useHotkeys = (
|
|||||||
triggerOnContentEditable?: boolean,
|
triggerOnContentEditable?: boolean,
|
||||||
) => {
|
) => {
|
||||||
const commandPaletteOpened = useAppStore((state) => state.commandPalette.opened);
|
const commandPaletteOpened = useAppStore((state) => state.commandPalette.opened);
|
||||||
|
const physicalHotkeys = useMemo(() => withPhysicalKeys(hotkeys), [hotkeys]);
|
||||||
|
|
||||||
useMantineHotkeys(
|
useMantineHotkeys(
|
||||||
commandPaletteOpened ? EMPTY_HOTKEYS : hotkeys,
|
commandPaletteOpened ? EMPTY_HOTKEYS : physicalHotkeys,
|
||||||
tagsToIgnore,
|
tagsToIgnore,
|
||||||
triggerOnContentEditable,
|
triggerOnContentEditable,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="Content-Security-Policy" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<title>Feishin</title>
|
<title>Feishin</title>
|
||||||
<% if (web) { %>
|
<% if (web) { %>
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ const SIDE_QUEUE_TYPES = new Set(['sideDrawerQueue', 'sideQueue']);
|
|||||||
const SIDE_QUEUE_LAYOUTS = new Set(['horizontal', 'vertical']);
|
const SIDE_QUEUE_LAYOUTS = new Set(['horizontal', 'vertical']);
|
||||||
const SIDEBAR_PLAYLIST_FOLDER_VIEWS = new Set(['navigation', 'single', 'tree']);
|
const SIDEBAR_PLAYLIST_FOLDER_VIEWS = new Set(['navigation', 'single', 'tree']);
|
||||||
const SIDEBAR_PLAYLIST_MODES = new Set(['compact', 'expanded']);
|
const SIDEBAR_PLAYLIST_MODES = new Set(['compact', 'expanded']);
|
||||||
|
const AUTO_DJ_MODES = new Set(['albums', 'songs']);
|
||||||
|
const AUTO_DJ_STRATEGIES = new Set(['library_random', 'similar']);
|
||||||
|
|
||||||
export type EnvSettingsOverrides = DeepPartial<
|
export type EnvSettingsOverrides = DeepPartial<
|
||||||
Pick<SettingsState, 'autoDJ' | 'css' | 'discord' | 'font' | 'general' | 'lyrics' | 'playback'>
|
Pick<SettingsState, 'autoDJ' | 'css' | 'discord' | 'font' | 'general' | 'lyrics' | 'playback'>
|
||||||
@@ -422,8 +424,21 @@ const ENV_SETTING_SPECS: EnvSettingSpec[] = [
|
|||||||
path: ['lyrics', 'alignment'],
|
path: ['lyrics', 'alignment'],
|
||||||
type: 'enum',
|
type: 'enum',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
enumSet: AUTO_DJ_STRATEGIES,
|
||||||
|
key: 'FS_AUTO_DJ_ALBUM_STRATEGY',
|
||||||
|
path: ['autoDJ', 'albumStrategy'],
|
||||||
|
type: 'enum',
|
||||||
|
},
|
||||||
{ key: 'FS_AUTO_DJ_ENABLED', path: ['autoDJ', 'enabled'], type: 'bool' },
|
{ key: 'FS_AUTO_DJ_ENABLED', path: ['autoDJ', 'enabled'], type: 'bool' },
|
||||||
{ key: 'FS_AUTO_DJ_ITEM_COUNT', path: ['autoDJ', 'itemCount'], type: 'num' },
|
{ key: 'FS_AUTO_DJ_ITEM_COUNT', path: ['autoDJ', 'itemCount'], type: 'num' },
|
||||||
|
{ enumSet: AUTO_DJ_MODES, key: 'FS_AUTO_DJ_MODE', path: ['autoDJ', 'mode'], type: 'enum' },
|
||||||
|
{
|
||||||
|
enumSet: AUTO_DJ_STRATEGIES,
|
||||||
|
key: 'FS_AUTO_DJ_SONG_STRATEGY',
|
||||||
|
path: ['autoDJ', 'songStrategy'],
|
||||||
|
type: 'enum',
|
||||||
|
},
|
||||||
{ key: 'FS_AUTO_DJ_TIMING', path: ['autoDJ', 'timing'], type: 'num' },
|
{ key: 'FS_AUTO_DJ_TIMING', path: ['autoDJ', 'timing'], type: 'num' },
|
||||||
{
|
{
|
||||||
key: 'FS_CSS_CONTENT',
|
key: 'FS_CSS_CONTENT',
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ function calculateNextIndex(
|
|||||||
} else {
|
} else {
|
||||||
// Repeat none: move to next track, or pause if at the end
|
// Repeat none: move to next track, or pause if at the end
|
||||||
if (isLastTrack) {
|
if (isLastTrack) {
|
||||||
return { nextIndex: 0, shouldPause: true };
|
return { nextIndex: currentIndex, shouldPause: true };
|
||||||
} else {
|
} else {
|
||||||
return { nextIndex: currentIndex + 1, shouldPause: false };
|
return { nextIndex: currentIndex + 1, shouldPause: false };
|
||||||
}
|
}
|
||||||
@@ -939,10 +939,12 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
const pauseOnNext = player.pauseOnNextSongEnd;
|
const pauseOnNext = player.pauseOnNextSongEnd;
|
||||||
const newStatus =
|
const newStatus =
|
||||||
shouldPause || pauseOnNext ? PlayerStatus.PAUSED : PlayerStatus.PLAYING;
|
shouldPause || pauseOnNext ? PlayerStatus.PAUSED : PlayerStatus.PLAYING;
|
||||||
|
const shouldKeepCurrentPlayer = newStatus === PlayerStatus.PAUSED;
|
||||||
|
const shouldSwapPlayer = !isRepeatOneSameTrack && !shouldKeepCurrentPlayer;
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.player.index = nextPlaybackIndex;
|
state.player.index = nextPlaybackIndex;
|
||||||
state.player.playerNum = newPlayerNum;
|
state.player.playerNum = shouldSwapPlayer ? newPlayerNum : player.playerNum;
|
||||||
setTimestampStore(0);
|
setTimestampStore(0);
|
||||||
state.player.status = newStatus;
|
state.player.status = newStatus;
|
||||||
|
|
||||||
@@ -999,7 +1001,7 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { player1, player2 } = getDualPlayerSongs(
|
const { player1, player2 } = getDualPlayerSongs(
|
||||||
newPlayerNum,
|
shouldSwapPlayer ? newPlayerNum : player.playerNum,
|
||||||
currentSong,
|
currentSong,
|
||||||
nextSong,
|
nextSong,
|
||||||
repeat,
|
repeat,
|
||||||
@@ -1009,7 +1011,7 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
currentSong,
|
currentSong,
|
||||||
index: currentQueueIndex,
|
index: currentQueueIndex,
|
||||||
nextSong,
|
nextSong,
|
||||||
num: newPlayerNum,
|
num: shouldSwapPlayer ? newPlayerNum : player.playerNum,
|
||||||
player1,
|
player1,
|
||||||
player2,
|
player2,
|
||||||
previousSong,
|
previousSong,
|
||||||
|
|||||||
@@ -675,9 +675,28 @@ const QueryBuilderSettingsSchema = z.object({
|
|||||||
tag: z.array(QueryBuilderCustomFieldSchema),
|
tag: z.array(QueryBuilderCustomFieldSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AUTO_DJ_MODE = {
|
||||||
|
ALBUMS: 'albums',
|
||||||
|
SONGS: 'songs',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type AutoDJMode = (typeof AUTO_DJ_MODE)[keyof typeof AUTO_DJ_MODE];
|
||||||
|
|
||||||
|
export const AUTO_DJ_STRATEGY = {
|
||||||
|
LIBRARY_RANDOM: 'library_random',
|
||||||
|
SIMILAR: 'similar',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type AutoDJStrategy = (typeof AUTO_DJ_STRATEGY)[keyof typeof AUTO_DJ_STRATEGY];
|
||||||
|
|
||||||
|
const autoDjStrategyEnum = z.enum(['similar', 'library_random']);
|
||||||
|
|
||||||
const AutoDJSettingsSchema = z.object({
|
const AutoDJSettingsSchema = z.object({
|
||||||
|
albumStrategy: autoDjStrategyEnum,
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
itemCount: z.number(),
|
itemCount: z.number(),
|
||||||
|
mode: z.enum(['songs', 'albums']),
|
||||||
|
songStrategy: autoDjStrategyEnum,
|
||||||
timing: z.number(),
|
timing: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1091,8 +1110,11 @@ const platformDefaultWindowBarStyle: Platform = getPlatformDefaultWindowBarStyle
|
|||||||
|
|
||||||
const initialState: SettingsState = {
|
const initialState: SettingsState = {
|
||||||
autoDJ: {
|
autoDJ: {
|
||||||
|
albumStrategy: AUTO_DJ_STRATEGY.SIMILAR,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
itemCount: 5,
|
itemCount: 5,
|
||||||
|
mode: 'songs',
|
||||||
|
songStrategy: AUTO_DJ_STRATEGY.SIMILAR,
|
||||||
timing: 1,
|
timing: 1,
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
@@ -2427,10 +2449,41 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version < 28) {
|
||||||
|
if (!state.autoDJ) {
|
||||||
|
state.autoDJ = { ...initialState.autoDJ };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.autoDJ.mode !== 'albums' && state.autoDJ.mode !== 'songs') {
|
||||||
|
state.autoDJ.mode = initialState.autoDJ.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeAutoDjStrategy = (stored: unknown) => {
|
||||||
|
if (stored === 'library_random') {
|
||||||
|
return AUTO_DJ_STRATEGY.LIBRARY_RANDOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
stored === 'similar' ||
|
||||||
|
stored === 'default' ||
|
||||||
|
stored === 'similar_forward'
|
||||||
|
) {
|
||||||
|
return AUTO_DJ_STRATEGY.SIMILAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialState.autoDJ.songStrategy;
|
||||||
|
};
|
||||||
|
|
||||||
|
state.autoDJ.songStrategy = normalizeAutoDjStrategy(state.autoDJ.songStrategy);
|
||||||
|
state.autoDJ.albumStrategy = normalizeAutoDjStrategy(
|
||||||
|
state.autoDJ.albumStrategy,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return persistedState;
|
return persistedState;
|
||||||
},
|
},
|
||||||
name: 'store_settings',
|
name: 'store_settings',
|
||||||
version: 27,
|
version: 28,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ export const UpdateAvailableDialog = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isElectron()) return;
|
if (!isElectron()) return;
|
||||||
|
|
||||||
const handleUpdateAvailable = (_event: any, newVersion: string) => {
|
const handleUpdateAvailable = (newVersion: string) => {
|
||||||
if (versionDismissed !== newVersion) {
|
if (versionDismissed !== newVersion) {
|
||||||
setVersion(newVersion);
|
setVersion(newVersion);
|
||||||
setOpened(true);
|
setOpened(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.api.ipc.on('update-available', handleUpdateAvailable);
|
window.api.utils.rendererUpdateAvailable(handleUpdateAvailable);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.api.ipc.removeListener?.('update-available', handleUpdateAvailable);
|
window.api.ipc.removeListener?.('update-available', handleUpdateAvailable);
|
||||||
|
|||||||
@@ -2,7 +2,17 @@ import {
|
|||||||
type HotkeyItem as MantineHotkeyItem,
|
type HotkeyItem as MantineHotkeyItem,
|
||||||
useHotkeys as useMantineHotkeys,
|
useHotkeys as useMantineHotkeys,
|
||||||
} from '@mantine/hooks';
|
} from '@mantine/hooks';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const useHotkeys = useMantineHotkeys;
|
import { withPhysicalKeys } from '/@/shared/utils/hotkeys';
|
||||||
|
|
||||||
|
export const useHotkeys = (
|
||||||
|
hotkeys: MantineHotkeyItem[],
|
||||||
|
tagsToIgnore?: string[],
|
||||||
|
triggerOnContentEditable?: boolean,
|
||||||
|
) => {
|
||||||
|
const physicalHotkeys = useMemo(() => withPhysicalKeys(hotkeys), [hotkeys]);
|
||||||
|
useMantineHotkeys(physicalHotkeys, tagsToIgnore, triggerOnContentEditable);
|
||||||
|
};
|
||||||
|
|
||||||
export type HotkeyItem = MantineHotkeyItem;
|
export type HotkeyItem = MantineHotkeyItem;
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { HotkeyItem } from '@mantine/hooks';
|
||||||
|
|
||||||
|
const RESERVED_KEYS = new Set(['alt', 'ctrl', 'meta', 'mod', 'shift']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts stored hotkey strings to Mantine's physical-key format.
|
||||||
|
* Mantine matches KeyboardEvent.code via normalizeKey, which turns Digit1 into
|
||||||
|
* "digit1" but leaves "1" as "1" — so mod+1 must become mod+Digit1.
|
||||||
|
*/
|
||||||
|
export const toPhysicalHotkey = (hotkey: string): string =>
|
||||||
|
hotkey
|
||||||
|
.split('+')
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.map((part) => {
|
||||||
|
if (part === '[plus]') {
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lower = part.toLowerCase();
|
||||||
|
if (RESERVED_KEYS.has(lower)) {
|
||||||
|
return lower;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\d$/.test(part)) {
|
||||||
|
return `Digit${part}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return part;
|
||||||
|
})
|
||||||
|
.join('+');
|
||||||
|
|
||||||
|
export const withPhysicalKeys = (hotkeys: HotkeyItem[]): HotkeyItem[] =>
|
||||||
|
hotkeys.map(([hotkey, handler, options]) => [
|
||||||
|
toPhysicalHotkey(hotkey),
|
||||||
|
handler,
|
||||||
|
{ ...options, usePhysicalKeys: true },
|
||||||
|
]);
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
const CODE_TO_HOTKEY_KEY: Record<string, string> = {
|
||||||
|
ArrowDown: 'arrowdown',
|
||||||
|
ArrowLeft: 'arrowleft',
|
||||||
|
ArrowRight: 'arrowright',
|
||||||
|
ArrowUp: 'arrowup',
|
||||||
|
Backspace: 'backspace',
|
||||||
|
Delete: 'delete',
|
||||||
|
End: 'end',
|
||||||
|
Enter: 'enter',
|
||||||
|
Equal: 'equal',
|
||||||
|
Escape: 'escape',
|
||||||
|
Home: 'home',
|
||||||
|
Insert: 'insert',
|
||||||
|
Minus: 'minus',
|
||||||
|
PageDown: 'pagedown',
|
||||||
|
PageUp: 'pageup',
|
||||||
|
Space: 'space',
|
||||||
|
Tab: 'tab',
|
||||||
|
};
|
||||||
|
|
||||||
|
const NUMPAD_CODE_TO_HOTKEY_KEY: Record<string, string> = {
|
||||||
|
Add: 'numpadadd',
|
||||||
|
Decimal: 'numpaddecimal',
|
||||||
|
Divide: 'numpaddivide',
|
||||||
|
Enter: 'numpadenter',
|
||||||
|
Multiply: 'numpadmultiply',
|
||||||
|
Subtract: 'numpadsubtract',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MODIFIER_KEY_CODES = new Set([
|
||||||
|
'AltLeft',
|
||||||
|
'AltRight',
|
||||||
|
'ControlLeft',
|
||||||
|
'ControlRight',
|
||||||
|
'MetaLeft',
|
||||||
|
'MetaRight',
|
||||||
|
'ShiftLeft',
|
||||||
|
'ShiftRight',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const keyboardCodeToHotkeyKey = (code: string): null | string => {
|
||||||
|
const mapped = CODE_TO_HOTKEY_KEY[code];
|
||||||
|
if (mapped) {
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code.startsWith('Key')) {
|
||||||
|
return code.slice(3).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code.startsWith('Digit')) {
|
||||||
|
return code.slice(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code.startsWith('Numpad')) {
|
||||||
|
const suffix = code.slice(6);
|
||||||
|
const numpadMapped = NUMPAD_CODE_TO_HOTKEY_KEY[suffix];
|
||||||
|
if (numpadMapped) {
|
||||||
|
return numpadMapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\d$/.test(suffix)) {
|
||||||
|
return `numpad${suffix}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user