mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b16e57710b | |||
| 931e96b9d1 | |||
| c27b86d2b2 | |||
| b3cf73836d | |||
| 1b15c73db0 | |||
| 4e53030e8d | |||
| 22b798812e | |||
| 1f7d510110 | |||
| 44fae06143 | |||
| 1e24e12a55 | |||
| 358bdec2b6 | |||
| 1b8661d566 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.21.0",
|
"version": "0.21.2",
|
||||||
"description": "A modern self-hosted music player.",
|
"description": "A modern self-hosted music player.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"subsonic",
|
"subsonic",
|
||||||
|
|||||||
@@ -483,7 +483,7 @@
|
|||||||
"discordRichPresence": "estat d'activitat de {{discord}}",
|
"discordRichPresence": "estat d'activitat de {{discord}}",
|
||||||
"discordRichPresence_description": "activa l'estat de reproducció a l'activitat de {{discord}}. Les tecles d'imatge són: {{icon}}, {{playing}} i {{paused}}",
|
"discordRichPresence_description": "activa l'estat de reproducció a l'activitat de {{discord}}. Les tecles d'imatge són: {{icon}}, {{playing}} i {{paused}}",
|
||||||
"discordServeImage": "serveix imatges de {{discord}} des del servidor",
|
"discordServeImage": "serveix imatges de {{discord}} des del servidor",
|
||||||
"discordServeImage_description": "comparteix la caràtula per l'estat d'activitat de {{discord}} des del servidor; només disponible per jellyfin i navidrome",
|
"discordServeImage_description": "comparteix la caràtula per l'estat d'activitat de {{discord}} des del servidor; només disponible per jellyfin i navidrome. {{discord}} fa ser un bot per trobar les imatges, de manera que el vostre servidor ha de ser visible per l'internet públic.",
|
||||||
"discordUpdateInterval": "interval d'actualització de l'estat d'activitat de {{discord}}",
|
"discordUpdateInterval": "interval d'actualització de l'estat d'activitat de {{discord}}",
|
||||||
"doubleClickBehavior": "posa en cua totes les pistes cercades en fer doble clic",
|
"doubleClickBehavior": "posa en cua totes les pistes cercades en fer doble clic",
|
||||||
"doubleClickBehavior_description": "si està actiu, totes les pistes coincidents en una cerca de pistes es posaran a la cua. altrament, només la que seleccioneu s'afegirà a la cua",
|
"doubleClickBehavior_description": "si està actiu, totes les pistes coincidents en una cerca de pistes es posaran a la cua. altrament, només la que seleccioneu s'afegirà a la cua",
|
||||||
@@ -653,7 +653,17 @@
|
|||||||
"discordLinkType": "enllaços d'estat de {{discord}}",
|
"discordLinkType": "enllaços d'estat de {{discord}}",
|
||||||
"discordLinkType_description": "afegeix enllaços externs a {{lastfm}} o {{musicbrainz}} als camps de cançó i artista a l'estat d'activitat de {{discord}}. {{musicbrainz}} és el més precís, però requereix etiquetes i no proporciona enllaços d'artista, mentre que {{lastfm}} hauria de propocionar un enllaç sempre. no fa sol·licituds de xarxa addicionals",
|
"discordLinkType_description": "afegeix enllaços externs a {{lastfm}} o {{musicbrainz}} als camps de cançó i artista a l'estat d'activitat de {{discord}}. {{musicbrainz}} és el més precís, però requereix etiquetes i no proporciona enllaços d'artista, mentre que {{lastfm}} hauria de propocionar un enllaç sempre. no fa sol·licituds de xarxa addicionals",
|
||||||
"discordLinkType_none": "$t(common.none)",
|
"discordLinkType_none": "$t(common.none)",
|
||||||
"discordLinkType_mbz_lastfm": "{{musicbrainz}} amb {{lastfm}} com a alternativa"
|
"discordLinkType_mbz_lastfm": "{{musicbrainz}} amb {{lastfm}} com a alternativa",
|
||||||
|
"artistBackground": "imatge de fons de l'artista",
|
||||||
|
"artistBackground_description": "afegeix una imatge de fons per les pàgines d'artista amb l'art de l'artista",
|
||||||
|
"artistBackgroundBlur": "mida del desenfocament de la imatge de fons de l'artista",
|
||||||
|
"artistBackgroundBlur_description": "ajusta la quantitat de desenfocament aplicat a la imatge de fons de l'artista",
|
||||||
|
"releaseChannel_optionLatest": "estable",
|
||||||
|
"releaseChannel_optionBeta": "beta",
|
||||||
|
"releaseChannel": "canal de versions",
|
||||||
|
"releaseChannel_description": "tria entre versions estables i versions beta per les actualitzacions automàtiques",
|
||||||
|
"mediaSession": "activa Media Session",
|
||||||
|
"mediaSession_description": "Activa la integració amb Windows Media Session per mostrar els controls multimèdia i les metadades a l'indicador de volum del sistema i la pantalla de bloqueig (només per Windows)"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"column": {
|
"column": {
|
||||||
|
|||||||
+76
-10
@@ -116,7 +116,8 @@
|
|||||||
"saveAs": "gorde honela",
|
"saveAs": "gorde honela",
|
||||||
"trackGain": "pista irabazpena",
|
"trackGain": "pista irabazpena",
|
||||||
"comingSoon": "laster…",
|
"comingSoon": "laster…",
|
||||||
"trackPeak": "pistaren gailurra"
|
"trackPeak": "pistaren gailurra",
|
||||||
|
"albumPeak": "albumaren gailurra"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
"repeat": "errepikatu",
|
"repeat": "errepikatu",
|
||||||
@@ -161,7 +162,10 @@
|
|||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"gap": "$t(common.gap)",
|
"gap": "$t(common.gap)",
|
||||||
"size": "$t(common.size)"
|
"size": "$t(common.size)",
|
||||||
|
"tableColumns": "taula zutabeak",
|
||||||
|
"itemSize": "elementuaren tamaina (px)",
|
||||||
|
"followCurrentSong": "jarraitu uneko abestia"
|
||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"actions": "$t(common.action_other)",
|
"actions": "$t(common.action_other)",
|
||||||
@@ -183,7 +187,13 @@
|
|||||||
"size": "$t(common.size)",
|
"size": "$t(common.size)",
|
||||||
"songCount": "$t(entity.track_other)",
|
"songCount": "$t(entity.track_other)",
|
||||||
"title": "$t(common.title)",
|
"title": "$t(common.title)",
|
||||||
"year": "$t(common.year)"
|
"year": "$t(common.year)",
|
||||||
|
"titleCombined": "$t(common.title) (batuta)",
|
||||||
|
"releaseDate": "argitalpen data",
|
||||||
|
"playCount": "erreprodukzio kopurua",
|
||||||
|
"lastPlayed": "azken aldiz entzundakoa",
|
||||||
|
"discNumber": "disko zenbakia",
|
||||||
|
"dateAdded": "gehitze data"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"column": {
|
"column": {
|
||||||
@@ -206,7 +216,11 @@
|
|||||||
"trackNumber": "pista",
|
"trackNumber": "pista",
|
||||||
"bpm": "bpm",
|
"bpm": "bpm",
|
||||||
"comment": "iruzkina",
|
"comment": "iruzkina",
|
||||||
"playCount": "erreprodukzioak"
|
"playCount": "erreprodukzioak",
|
||||||
|
"releaseDate": "argitalpen data",
|
||||||
|
"lastPlayed": "azken aldiz entzundakoa",
|
||||||
|
"dateAdded": "gehitutako data",
|
||||||
|
"albumArtist": "albumeko artista"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
@@ -397,7 +411,7 @@
|
|||||||
"discordRichPresence": "{{discord}} jarduera-egoera",
|
"discordRichPresence": "{{discord}} jarduera-egoera",
|
||||||
"discordRichPresence_description": "gaitu erreprodukzioa egoera {{discord}}-en jarduera-egoeran. Irudi gakoak hauek dira: {{icon}}, {{playing}}, eta {{paused}}",
|
"discordRichPresence_description": "gaitu erreprodukzioa egoera {{discord}}-en jarduera-egoeran. Irudi gakoak hauek dira: {{icon}}, {{playing}}, eta {{paused}}",
|
||||||
"discordServeImage": "zerbitzatu {{discord}} irudiak zerbitzaritik",
|
"discordServeImage": "zerbitzatu {{discord}} irudiak zerbitzaritik",
|
||||||
"discordServeImage_description": "partekatu {{discord}} jarduera-egoerarentzako azala artea zerbitzaritik bertatik, Jellyfin eta Navidrome-rentzat bakarrik eskuragarri",
|
"discordServeImage_description": "partekatu {{discord}} jarduera-egoerarentzako azala artea zerbitzaritik bertatik, Jellyfin eta Navidrome-rentzat bakarrik eskuragarri. {{discord}}-(e)k bot bat erabiltzen du irudiak eskuratzeko, beraz, zure zerbitzaria internet publikotik eskuragarri egon behar da.",
|
||||||
"discordUpdateInterval": "{{discord}} jarduera-egoera eguneraketa tartea",
|
"discordUpdateInterval": "{{discord}} jarduera-egoera eguneraketa tartea",
|
||||||
"discordLinkType_none": "$t(common.none)",
|
"discordLinkType_none": "$t(common.none)",
|
||||||
"albumBackground": "albumaren atzeko planoaren irudia",
|
"albumBackground": "albumaren atzeko planoaren irudia",
|
||||||
@@ -413,7 +427,55 @@
|
|||||||
"discordDisplayType_songname": "abesti izena",
|
"discordDisplayType_songname": "abesti izena",
|
||||||
"discordDisplayType_artistname": "artista izena(k)",
|
"discordDisplayType_artistname": "artista izena(k)",
|
||||||
"fontType_optionBuiltIn": "barneko letra-tipoa",
|
"fontType_optionBuiltIn": "barneko letra-tipoa",
|
||||||
"hotkey_globalSearch": "bilaketa globala"
|
"hotkey_globalSearch": "bilaketa globala",
|
||||||
|
"albumBackgroundBlur_description": "albumaren atzeko planoaren irudiari aplikatzen zaion lausotze-kopurua doitzen du",
|
||||||
|
"artistBackground": "artistaren atzeko planoaren irudia",
|
||||||
|
"artistBackgroundBlur": "artistaren atzeko planoko irudiaren lausotze-tamaina",
|
||||||
|
"artistBackgroundBlur_description": "artistaren atzeko planoaren irudiari aplikatzen zaion lausotze-kopurua doitzen du",
|
||||||
|
"artistConfiguration": "albumaren artistaren konfigurazio orria",
|
||||||
|
"artistConfiguration_description": "konfiguratu zein elementu erakusten diren eta zein ordenatan albumaren artistaren orrian",
|
||||||
|
"audioExclusiveMode": "audio esklusiboko modua",
|
||||||
|
"releaseChannel_optionLatest": "egonkorra",
|
||||||
|
"releaseChannel_optionBeta": "beta",
|
||||||
|
"releaseChannel": "argitalpen kanala",
|
||||||
|
"releaseChannel_description": "aukeratu argitalpen egonkorren edo beta artean eguneratze automatikoak lortzeko",
|
||||||
|
"discordUpdateInterval_description": "eguneratze bakoitzaren arteko denbora segundotan (gutxienez 15 segundo)",
|
||||||
|
"discordDisplayType": "{{discord}} jarduera-pantailaren mota",
|
||||||
|
"discordDisplayType_description": "zure egoeran entzuten ari zarena aldatzen du",
|
||||||
|
"discordLinkType": "{{discord}} egoera estekak",
|
||||||
|
"fontType_description": "barneko letra-tipoa Feishinek eskaintzen dituen letra-tipoetako bat aukeratzen du. sistemaren letra-tipoa zure sistema eragileak eskaintzen duen edozein letra-tipo hautatzeko aukera ematen dizu. pertsonalizatua zure letra-tipoa eskaintzeko aukera ematen dizu",
|
||||||
|
"genreBehavior": "genero orriaren portaera lehenetsia",
|
||||||
|
"homeConfiguration_description": "konfiguratu zein elementu erakusten diren hasierako orrian eta zein ordenatan",
|
||||||
|
"homeFeature": "etxeko karrusela nabarmendua",
|
||||||
|
"homeFeature_description": "hasierako orrian karrusel nabarmen handia erakutsi behar den ala ez kontrolatzen du",
|
||||||
|
"hotkey_localSearch": "orrian bilatu",
|
||||||
|
"hotkey_rate0": "garbitu balorazioa",
|
||||||
|
"hotkey_rate1": "1 izarretako balorazioa",
|
||||||
|
"hotkey_rate2": "2 izarretako balorazioa",
|
||||||
|
"hotkey_rate3": "3 izarretako balorazioa",
|
||||||
|
"hotkey_rate4": "4 izarretako balorazioa",
|
||||||
|
"hotkey_rate5": "5 izarretako balorazioa",
|
||||||
|
"zoom_description": "aplikazioaren zoom ehunekoa ezartzen du",
|
||||||
|
"zoom": "zoom ehunekoa",
|
||||||
|
"windowBarStyle_description": "aukeratu leiho-barraren estiloa",
|
||||||
|
"windowBarStyle": "leiho-barra estiloa",
|
||||||
|
"webAudio": "erabili web audioa",
|
||||||
|
"useSystemTheme_description": "jarraitu sistemak definitutako argi edo iluntasun lehentasuna",
|
||||||
|
"useSystemTheme": "erabili sistemaren gaia",
|
||||||
|
"translationTargetLanguage_description": "itzulpenerako helburu-hizkuntza",
|
||||||
|
"translationTargetLanguage": "itzulpenerako helburu-hizkuntza",
|
||||||
|
"translationApiKey": "itzulpen api gakoa",
|
||||||
|
"translationApiProvider_description": "itzulpenerako api hornitzailea",
|
||||||
|
"translationApiProvider": "itzulpen api hornitzailea",
|
||||||
|
"mediaSession": "gaitu multimedia saioa",
|
||||||
|
"themeLight_description": "aplikaziorako erabiliko den gaia argia ezartzen du",
|
||||||
|
"themeLight": "gaia (argia)",
|
||||||
|
"themeDark_description": "aplikaziorako erabiliko den gai iluna ezartzen du",
|
||||||
|
"themeDark": "gaia (iluna)",
|
||||||
|
"theme_description": "aplikaziorako erabiliko den gaia ezartzen du",
|
||||||
|
"externalLinks": "kanpoko estekak erakutsi",
|
||||||
|
"externalLinks_description": "kanpoko estekak (Last.fm, MusicBrainz) artista/album orrietan erakustea gaitzen du",
|
||||||
|
"exitToTray": "irten erretilura"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"addServer": {
|
"addServer": {
|
||||||
@@ -426,7 +488,8 @@
|
|||||||
"title": "zerbitzaria gehitu",
|
"title": "zerbitzaria gehitu",
|
||||||
"ignoreCors": "alde batera utzi cors $t(common.restartRequired)",
|
"ignoreCors": "alde batera utzi cors $t(common.restartRequired)",
|
||||||
"ignoreSsl": "alde batera utzi ssl $t(common.restartRequired)",
|
"ignoreSsl": "alde batera utzi ssl $t(common.restartRequired)",
|
||||||
"input_legacyAuthentication": "gaitu zaharkitutako autentifikazioa"
|
"input_legacyAuthentication": "gaitu zaharkitutako autentifikazioa",
|
||||||
|
"success": "zerbitzaria behar bezala gehitu da"
|
||||||
},
|
},
|
||||||
"addToPlaylist": {
|
"addToPlaylist": {
|
||||||
"input_playlists": "$t(entity.playlist_other)",
|
"input_playlists": "$t(entity.playlist_other)",
|
||||||
@@ -462,7 +525,8 @@
|
|||||||
},
|
},
|
||||||
"editPlaylist": {
|
"editPlaylist": {
|
||||||
"success": "$t(entity.playlist_one) behar bezala eguneratu da",
|
"success": "$t(entity.playlist_one) behar bezala eguneratu da",
|
||||||
"title": "$t(entity.playlist_one) editatu"
|
"title": "$t(entity.playlist_one) editatu",
|
||||||
|
"publicJellyfinNote": "Arrazoiren batengatik, Jellyfin ez du erakusten erreprodukzio-zerrendak publikoak diren edo ez. Hau publiko izaten jarraitzea nahi baduzu, hautatu sarrera hau"
|
||||||
},
|
},
|
||||||
"queryEditor": {
|
"queryEditor": {
|
||||||
"title": "kontsulta editorea",
|
"title": "kontsulta editorea",
|
||||||
@@ -504,7 +568,8 @@
|
|||||||
"privateModeOff": "itzali modu pribatua",
|
"privateModeOff": "itzali modu pribatua",
|
||||||
"privateModeOn": "aktibatu modu pribatua",
|
"privateModeOn": "aktibatu modu pribatua",
|
||||||
"selectServer": "aukeratu zerbitzaria",
|
"selectServer": "aukeratu zerbitzaria",
|
||||||
"version": "bertsioa {{version}}"
|
"version": "bertsioa {{version}}",
|
||||||
|
"openBrowserDevtools": "ireki nabigatzailearen garapen tresnak"
|
||||||
},
|
},
|
||||||
"manageServers": {
|
"manageServers": {
|
||||||
"url": "URLa",
|
"url": "URLa",
|
||||||
@@ -552,7 +617,8 @@
|
|||||||
"dynamicImageBlur": "irudiaren lausotze tamaina",
|
"dynamicImageBlur": "irudiaren lausotze tamaina",
|
||||||
"lyricAlignment": "letraren lerrokatzea",
|
"lyricAlignment": "letraren lerrokatzea",
|
||||||
"showLyricMatch": "erakutsi letren bat-etortzea",
|
"showLyricMatch": "erakutsi letren bat-etortzea",
|
||||||
"showLyricProvider": "erakutsi letra hornitzailea"
|
"showLyricProvider": "erakutsi letra hornitzailea",
|
||||||
|
"lyricOffset": "letra-desplazamendua (ms)"
|
||||||
},
|
},
|
||||||
"lyrics": "letrak",
|
"lyrics": "letrak",
|
||||||
"related": "erlazionatuta",
|
"related": "erlazionatuta",
|
||||||
|
|||||||
@@ -142,7 +142,9 @@
|
|||||||
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||||
"playShuffled": "$t(player.shuffle)",
|
"playShuffled": "$t(player.shuffle)",
|
||||||
"shareItem": "分享項目",
|
"shareItem": "分享項目",
|
||||||
"showDetails": "取得資訊"
|
"showDetails": "取得資訊",
|
||||||
|
"goToAlbum": "前往 $t(entity.album_one)",
|
||||||
|
"goToAlbumArtist": "前往 $t(entity.albumArtist_one)"
|
||||||
},
|
},
|
||||||
"globalSearch": {
|
"globalSearch": {
|
||||||
"title": "指令",
|
"title": "指令",
|
||||||
@@ -157,7 +159,8 @@
|
|||||||
"recentlyPlayed": "最近播放",
|
"recentlyPlayed": "最近播放",
|
||||||
"title": "$t(common.home)",
|
"title": "$t(common.home)",
|
||||||
"mostPlayed": "最多播放",
|
"mostPlayed": "最多播放",
|
||||||
"newlyAdded": "最近新增的發行"
|
"newlyAdded": "最近新增的發行",
|
||||||
|
"recentlyReleased": "最近發佈"
|
||||||
},
|
},
|
||||||
"appMenu": {
|
"appMenu": {
|
||||||
"openBrowserDevtools": "打開瀏覽器開發者工具",
|
"openBrowserDevtools": "打開瀏覽器開發者工具",
|
||||||
@@ -169,7 +172,9 @@
|
|||||||
"selectServer": "選擇伺服器",
|
"selectServer": "選擇伺服器",
|
||||||
"settings": "$t(common.setting_other)",
|
"settings": "$t(common.setting_other)",
|
||||||
"version": "版本 {{version}}",
|
"version": "版本 {{version}}",
|
||||||
"manageServers": "管理伺服器"
|
"manageServers": "管理伺服器",
|
||||||
|
"privateModeOff": "關閉私人模式",
|
||||||
|
"privateModeOn": "開啟私人模式"
|
||||||
},
|
},
|
||||||
"fullscreenPlayer": {
|
"fullscreenPlayer": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -489,7 +494,7 @@
|
|||||||
"discordListening": "將狀態設為\"正在聽\"",
|
"discordListening": "將狀態設為\"正在聽\"",
|
||||||
"discordListening_description": "將狀態顯示為\"正在聽\"而不是\"正在玩\"",
|
"discordListening_description": "將狀態顯示為\"正在聽\"而不是\"正在玩\"",
|
||||||
"discordServeImage": "從伺服器提供{{discord}}圖片",
|
"discordServeImage": "從伺服器提供{{discord}}圖片",
|
||||||
"discordServeImage_description": "從伺服器本身為 {{discord}} rich presence 分享封面圖片,只在jellyfin和navidrome可用",
|
"discordServeImage_description": "從伺服器本身分享 {{discord}} Rich Presence的封面圖片,僅支援 Jellyfin 與 Navidrome。{{discord}} 會透過機器人擷取圖片,因此您的伺服器必須能從公開網路連線。",
|
||||||
"doubleClickBehavior": "雙擊時將所有搜尋到的曲目加入佇列",
|
"doubleClickBehavior": "雙擊時將所有搜尋到的曲目加入佇列",
|
||||||
"doubleClickBehavior_description": "如果為 true,則歌曲搜尋中所有符合的歌曲都會被加入佇列。否則,只有被點擊的歌曲才會被加入佇列",
|
"doubleClickBehavior_description": "如果為 true,則歌曲搜尋中所有符合的歌曲都會被加入佇列。否則,只有被點擊的歌曲才會被加入佇列",
|
||||||
"externalLinks": "顯示外部連結",
|
"externalLinks": "顯示外部連結",
|
||||||
@@ -544,7 +549,27 @@
|
|||||||
"webAudio": "使用網頁音訊",
|
"webAudio": "使用網頁音訊",
|
||||||
"webAudio_description": "使用網頁音訊。這將啟用重播增益等進階功能。如果您遇到其他問題,請停用",
|
"webAudio_description": "使用網頁音訊。這將啟用重播增益等進階功能。如果您遇到其他問題,請停用",
|
||||||
"preservePitch": "保持音高",
|
"preservePitch": "保持音高",
|
||||||
"preservePitch_description": "修改播放速度時保留音調"
|
"preservePitch_description": "修改播放速度時保留音調",
|
||||||
|
"artistBackground": "藝人背景圖片",
|
||||||
|
"artistBackground_description": "為藝人頁面新增含藝人圖片的背景圖像",
|
||||||
|
"artistBackgroundBlur": "藝人背景圖片模糊程度",
|
||||||
|
"artistBackgroundBlur_description": "調整套用至藝人背景圖片的模糊程度",
|
||||||
|
"releaseChannel_optionLatest": "穩定版",
|
||||||
|
"releaseChannel_optionBeta": "測試版",
|
||||||
|
"releaseChannel_description": "選擇自動更新時要使用穩定版本或是測試版本",
|
||||||
|
"discordDisplayType": "{{discord}} presence 顯示類型",
|
||||||
|
"discordDisplayType_description": "變更您在狀態中正在聆聽的內容",
|
||||||
|
"discordDisplayType_songname": "歌曲名稱",
|
||||||
|
"discordDisplayType_artistname": "藝人名稱",
|
||||||
|
"discordLinkType": "{{discord}} presence 連結",
|
||||||
|
"discordLinkType_description": "在 {{discord}} Rich Presence中,為歌曲和藝人欄位新增 {{lastfm}} 或 {{musicbrainz}} 的外部連結。{{musicbrainz}} 的準確度最高,但需要標籤且不提供藝人連結;而 {{lastfm}} 通常都能提供連結。此功能不會產生額外的網路請求",
|
||||||
|
"discordLinkType_none": "$t(common.none)",
|
||||||
|
"discordLinkType_mbz_lastfm": "{{musicbrainz}} 並以 {{lastfm}} 備用",
|
||||||
|
"hotkey_navigateHome": "導航至首頁",
|
||||||
|
"preventSleepOnPlayback": "防止播放時進入睡眠狀態",
|
||||||
|
"preventSleepOnPlayback_description": "在音樂播放時防止螢幕進入睡眠狀態",
|
||||||
|
"mediaSession": "啟用Media Session",
|
||||||
|
"mediaSession_description": "啟用 Windows Media Session 整合功能,於系統音量Overlay和鎖定畫面中顯示媒體資料與控制面板(僅限 Windows)"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -723,7 +748,9 @@
|
|||||||
"title": "新增伺服器",
|
"title": "新增伺服器",
|
||||||
"error_savePassword": "儲存密碼時出現錯誤",
|
"error_savePassword": "儲存密碼時出現錯誤",
|
||||||
"ignoreCors": "忽略 cors $t(common.restartRequired)",
|
"ignoreCors": "忽略 cors $t(common.restartRequired)",
|
||||||
"ignoreSsl": "忽略 ssl $t(common.restartRequired)"
|
"ignoreSsl": "忽略 ssl $t(common.restartRequired)",
|
||||||
|
"input_preferInstantMix": "偏好即時混音",
|
||||||
|
"input_preferInstantMixDescription": "僅使用即時混音功能來獲取相似歌曲。若您擁有能修改此行為的外掛,此功能將相當實用"
|
||||||
},
|
},
|
||||||
"addToPlaylist": {
|
"addToPlaylist": {
|
||||||
"input_playlists": "$t(entity.playlist_other)",
|
"input_playlists": "$t(entity.playlist_other)",
|
||||||
@@ -770,6 +797,11 @@
|
|||||||
"success": "分享連結已複製到剪貼簿(或點擊此處開啟)",
|
"success": "分享連結已複製到剪貼簿(或點擊此處開啟)",
|
||||||
"expireInvalid": "到期日必須是未來",
|
"expireInvalid": "到期日必須是未來",
|
||||||
"createFailed": "無法建立分享(分享是否啟用?)"
|
"createFailed": "無法建立分享(分享是否啟用?)"
|
||||||
|
},
|
||||||
|
"privateMode": {
|
||||||
|
"enabled": "已啟用私人模式,播放狀態將對外部整合隱藏",
|
||||||
|
"disabled": "已停用私人模式,播放狀態現對已啟用的外部整合可見",
|
||||||
|
"title": "私人模式"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,17 +39,21 @@ interface CenterControlsProps {
|
|||||||
export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [isSeeking, setIsSeeking] = useState(false);
|
||||||
const currentSong = useCurrentSong();
|
const currentSong = useCurrentSong();
|
||||||
const skip = useSettingsStore((state) => state.general.skipButtons);
|
const skip = useSettingsStore((state) => state.general.skipButtons);
|
||||||
const buttonSize = useSettingsStore((state) => state.general.buttonSize);
|
const buttonSize = useSettingsStore((state) => state.general.buttonSize);
|
||||||
|
const playbackType = usePlaybackType();
|
||||||
const player1 = playersRef?.current?.player1;
|
const player1 = playersRef?.current?.player1;
|
||||||
const player2 = playersRef?.current?.player2;
|
const player2 = playersRef?.current?.player2;
|
||||||
const status = useCurrentStatus();
|
const status = useCurrentStatus();
|
||||||
|
const player = useCurrentPlayer();
|
||||||
|
const setCurrentTime = useSetCurrentTime();
|
||||||
const repeat = useRepeatStatus();
|
const repeat = useRepeatStatus();
|
||||||
const shuffle = useShuffleStatus();
|
const shuffle = useShuffleStatus();
|
||||||
const { bindings } = useHotkeySettings();
|
const { bindings } = useHotkeySettings();
|
||||||
|
const { showTimeRemaining } = useAppStore();
|
||||||
|
const { setShowTimeRemaining } = useAppStoreActions();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleNextTrack,
|
handleNextTrack,
|
||||||
@@ -57,6 +61,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
|||||||
handlePlay,
|
handlePlay,
|
||||||
handlePlayPause,
|
handlePlayPause,
|
||||||
handlePrevTrack,
|
handlePrevTrack,
|
||||||
|
handleSeekSlider,
|
||||||
handleSkipBackward,
|
handleSkipBackward,
|
||||||
handleSkipForward,
|
handleSkipForward,
|
||||||
handleStop,
|
handleStop,
|
||||||
@@ -65,6 +70,32 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
|||||||
} = useCenterControls({ playersRef });
|
} = useCenterControls({ playersRef });
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
|
|
||||||
|
const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0;
|
||||||
|
const currentTime = useCurrentTime();
|
||||||
|
const currentPlayerRef = player === 1 ? player1 : player2;
|
||||||
|
const formattedDuration = formatDuration(songDuration * 1000 || 0);
|
||||||
|
const formattedTimeRemaining = formatDuration((currentTime - songDuration) * 1000 || 0);
|
||||||
|
const formattedTime = formatDuration(currentTime * 1000 || 0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
|
if (status === PlayerStatus.PLAYING && !isSeeking) {
|
||||||
|
if (!isElectron() || playbackType === PlaybackType.WEB) {
|
||||||
|
// Update twice a second for slightly better performance
|
||||||
|
interval = setInterval(() => {
|
||||||
|
if (currentPlayerRef) {
|
||||||
|
setCurrentTime(currentPlayerRef.getCurrentTime());
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [currentPlayerRef, isSeeking, setCurrentTime, playbackType, status]);
|
||||||
|
|
||||||
|
const [seekValue, setSeekValue] = useState(0);
|
||||||
|
|
||||||
useHotkeys([
|
useHotkeys([
|
||||||
[bindings.playPause.isGlobal ? '' : bindings.playPause.hotkey, handlePlayPause],
|
[bindings.playPause.isGlobal ? '' : bindings.playPause.hotkey, handlePlayPause],
|
||||||
[bindings.play.isGlobal ? '' : bindings.play.hotkey, handlePlay],
|
[bindings.play.isGlobal ? '' : bindings.play.hotkey, handlePlay],
|
||||||
@@ -84,7 +115,16 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useMediaSession(playersRef);
|
useMediaSession({
|
||||||
|
handleNextTrack,
|
||||||
|
handlePause,
|
||||||
|
handlePlay,
|
||||||
|
handlePrevTrack,
|
||||||
|
handleSeekSlider,
|
||||||
|
handleSkipBackward,
|
||||||
|
handleSkipForward,
|
||||||
|
handleStop,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -227,108 +267,57 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PlayerSeekSlider player1={player1} player2={player2} playersRef={playersRef} />
|
<div className={styles.sliderContainer}>
|
||||||
|
<div className={styles.sliderValueWrapper}>
|
||||||
|
<Text
|
||||||
|
className={PlaybackSelectors.elapsedTime}
|
||||||
|
fw={600}
|
||||||
|
isMuted
|
||||||
|
isNoSelect
|
||||||
|
size="xs"
|
||||||
|
style={{ userSelect: 'none' }}
|
||||||
|
>
|
||||||
|
{formattedTime}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className={styles.sliderWrapper}>
|
||||||
|
<PlayerbarSlider
|
||||||
|
label={(value) => formatDuration(value * 1000)}
|
||||||
|
max={songDuration}
|
||||||
|
min={0}
|
||||||
|
onChange={(e) => {
|
||||||
|
setIsSeeking(true);
|
||||||
|
setSeekValue(e);
|
||||||
|
}}
|
||||||
|
onChangeEnd={(e) => {
|
||||||
|
// There is a timing bug in Mantine in which the onChangeEnd
|
||||||
|
// event fires before onChange. Add a small delay to force
|
||||||
|
// onChangeEnd to happen after onCHange
|
||||||
|
setTimeout(() => {
|
||||||
|
handleSeekSlider(e);
|
||||||
|
setIsSeeking(false);
|
||||||
|
}, 50);
|
||||||
|
}}
|
||||||
|
size={6}
|
||||||
|
value={!isSeeking ? currentTime : seekValue}
|
||||||
|
w="100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.sliderValueWrapper}>
|
||||||
|
<Text
|
||||||
|
className={PlaybackSelectors.totalDuration}
|
||||||
|
fw={600}
|
||||||
|
isMuted
|
||||||
|
isNoSelect
|
||||||
|
onClick={() => setShowTimeRemaining(!showTimeRemaining)}
|
||||||
|
role="button"
|
||||||
|
size="xs"
|
||||||
|
style={{ cursor: 'pointer', userSelect: 'none' }}
|
||||||
|
>
|
||||||
|
{showTimeRemaining ? formattedTimeRemaining : formattedDuration}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PlayerSeekSlider = ({
|
|
||||||
player1,
|
|
||||||
player2,
|
|
||||||
playersRef,
|
|
||||||
}: {
|
|
||||||
player1: any;
|
|
||||||
player2: any;
|
|
||||||
playersRef: any;
|
|
||||||
}) => {
|
|
||||||
const { handleSeekSlider } = useCenterControls({ playersRef });
|
|
||||||
|
|
||||||
const player = useCurrentPlayer();
|
|
||||||
const playbackType = usePlaybackType();
|
|
||||||
const setCurrentTime = useSetCurrentTime();
|
|
||||||
const status = useCurrentStatus();
|
|
||||||
|
|
||||||
const currentSong = useCurrentSong();
|
|
||||||
const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0;
|
|
||||||
const currentTime = useCurrentTime();
|
|
||||||
const currentPlayerRef = player === 1 ? player1 : player2;
|
|
||||||
const [isSeeking, setIsSeeking] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let interval: ReturnType<typeof setInterval>;
|
|
||||||
|
|
||||||
if (status === PlayerStatus.PLAYING && !isSeeking) {
|
|
||||||
if (!isElectron() || playbackType === PlaybackType.WEB) {
|
|
||||||
// Update twice a second for slightly better performance
|
|
||||||
interval = setInterval(() => {
|
|
||||||
if (currentPlayerRef) {
|
|
||||||
setCurrentTime(currentPlayerRef.getCurrentTime());
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [currentPlayerRef, isSeeking, setCurrentTime, playbackType, status]);
|
|
||||||
|
|
||||||
const { showTimeRemaining } = useAppStore();
|
|
||||||
const { setShowTimeRemaining } = useAppStoreActions();
|
|
||||||
const formattedDuration = formatDuration(songDuration * 1000 || 0);
|
|
||||||
const formattedTimeRemaining = formatDuration((currentTime - songDuration) * 1000 || 0);
|
|
||||||
const formattedTime = formatDuration(currentTime * 1000 || 0);
|
|
||||||
const [seekValue, setSeekValue] = useState(0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.sliderContainer}>
|
|
||||||
<div className={styles.sliderValueWrapper}>
|
|
||||||
<Text
|
|
||||||
className={PlaybackSelectors.elapsedTime}
|
|
||||||
fw={600}
|
|
||||||
isMuted
|
|
||||||
isNoSelect
|
|
||||||
size="xs"
|
|
||||||
style={{ userSelect: 'none' }}
|
|
||||||
>
|
|
||||||
{formattedTime}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className={styles.sliderWrapper}>
|
|
||||||
<PlayerbarSlider
|
|
||||||
label={(value) => formatDuration(value * 1000)}
|
|
||||||
max={songDuration}
|
|
||||||
min={0}
|
|
||||||
onChange={(e) => {
|
|
||||||
setIsSeeking(true);
|
|
||||||
setSeekValue(e);
|
|
||||||
}}
|
|
||||||
onChangeEnd={(e) => {
|
|
||||||
// There is a timing bug in Mantine in which the onChangeEnd
|
|
||||||
// event fires before onChange. Add a small delay to force
|
|
||||||
// onChangeEnd to happen after onCHange
|
|
||||||
setTimeout(() => {
|
|
||||||
handleSeekSlider(e);
|
|
||||||
setIsSeeking(false);
|
|
||||||
}, 50);
|
|
||||||
}}
|
|
||||||
size={6}
|
|
||||||
value={!isSeeking ? currentTime : seekValue}
|
|
||||||
w="100%"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.sliderValueWrapper}>
|
|
||||||
<Text
|
|
||||||
className={PlaybackSelectors.totalDuration}
|
|
||||||
fw={600}
|
|
||||||
isMuted
|
|
||||||
isNoSelect
|
|
||||||
onClick={() => setShowTimeRemaining(!showTimeRemaining)}
|
|
||||||
role="button"
|
|
||||||
size="xs"
|
|
||||||
style={{ cursor: 'pointer', userSelect: 'none' }}
|
|
||||||
>
|
|
||||||
{showTimeRemaining ? formattedTimeRemaining : formattedDuration}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { useCenterControls } from '/@/renderer/features/player/hooks/use-center-controls';
|
|
||||||
import {
|
import {
|
||||||
useCurrentSong,
|
useCurrentSong,
|
||||||
useCurrentStatus,
|
useCurrentStatus,
|
||||||
@@ -9,26 +8,31 @@ import {
|
|||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { PlayerStatus } from '/@/shared/types/types';
|
import { PlayerStatus } from '/@/shared/types/types';
|
||||||
|
|
||||||
export const useMediaSession = (playersRef: { player1: any; player2: any }) => {
|
export const useMediaSession = ({
|
||||||
|
handleNextTrack,
|
||||||
|
handlePause,
|
||||||
|
handlePlay,
|
||||||
|
handlePrevTrack,
|
||||||
|
handleSeekSlider,
|
||||||
|
handleSkipBackward,
|
||||||
|
handleSkipForward,
|
||||||
|
handleStop,
|
||||||
|
}: {
|
||||||
|
handleNextTrack: () => void;
|
||||||
|
handlePause: () => void;
|
||||||
|
handlePlay: () => void;
|
||||||
|
handlePrevTrack: () => void;
|
||||||
|
handleSeekSlider: (e: any | number) => void;
|
||||||
|
handleSkipBackward: (seconds: number) => void;
|
||||||
|
handleSkipForward: (seconds: number) => void;
|
||||||
|
handleStop: () => void;
|
||||||
|
}) => {
|
||||||
const { mediaSession: mediaSessionEnabled } = usePlaybackSettings();
|
const { mediaSession: mediaSessionEnabled } = usePlaybackSettings();
|
||||||
const playerStatus = useCurrentStatus();
|
const playerStatus = useCurrentStatus();
|
||||||
const currentSong = useCurrentSong();
|
const currentSong = useCurrentSong();
|
||||||
const mediaSession = navigator.mediaSession;
|
const mediaSession = navigator.mediaSession;
|
||||||
const skip = useSettingsStore((state) => state.general.skipButtons);
|
const skip = useSettingsStore((state) => state.general.skipButtons);
|
||||||
|
|
||||||
const {
|
|
||||||
handleNextTrack,
|
|
||||||
handlePause,
|
|
||||||
handlePlay,
|
|
||||||
handlePrevTrack,
|
|
||||||
handleSeekSlider,
|
|
||||||
handleSkipBackward,
|
|
||||||
handleSkipForward,
|
|
||||||
handleStop,
|
|
||||||
} = useCenterControls({
|
|
||||||
playersRef,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mediaSessionEnabled || !mediaSession) {
|
if (!mediaSessionEnabled || !mediaSession) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import {
|
|||||||
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
import { Switch } from '/@/shared/components/switch/switch';
|
||||||
|
|
||||||
const isWindows = window.api.utils.isWindows();
|
const isWindows = isElectron() ? window.api.utils.isWindows() : null;
|
||||||
const isDesktop = isElectron();
|
const isDesktop = isElectron();
|
||||||
|
const ipc = isElectron() ? window.api.ipc : null;
|
||||||
|
|
||||||
export const MediaSessionSettings = () => {
|
export const MediaSessionSettings = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -19,7 +20,7 @@ export const MediaSessionSettings = () => {
|
|||||||
function handleMediaSessionChange() {
|
function handleMediaSessionChange() {
|
||||||
const current = mediaSession;
|
const current = mediaSession;
|
||||||
toggleMediaSession();
|
toggleMediaSession();
|
||||||
window.api.ipc.send('settings-set', { property: 'mediaSession', value: !current });
|
ipc?.send('settings-set', { property: 'mediaSession', value: !current });
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaSessionOptions: SettingOption[] = [
|
const mediaSessionOptions: SettingOption[] = [
|
||||||
@@ -35,7 +36,7 @@ export const MediaSessionSettings = () => {
|
|||||||
context: 'description',
|
context: 'description',
|
||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
}),
|
}),
|
||||||
isHidden: isDesktop && !isWindows,
|
isHidden: !isWindows || !isDesktop,
|
||||||
note: t('common.restartRequired', { postProcess: 'sentenceCase' }),
|
note: t('common.restartRequired', { postProcess: 'sentenceCase' }),
|
||||||
title: t('setting.mediaSession', { postProcess: 'sentenceCase' }),
|
title: t('setting.mediaSession', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user