Merge pull request #625 from Zagrios/chore/feat-move-create-playlist-button/521

[chore-521] Move create playlist action
This commit is contained in:
MathieuG-P
2024-10-22 13:57:37 +02:00
committed by GitHub
24 changed files with 703 additions and 109 deletions
+41 -5
View File
@@ -52,8 +52,9 @@
"tabs": {
"maps": {
"actions": {
"add-maps": {
"text": "Hinzufügen"
"drop-down": {
"browse-maps": "Karten durchsuchen",
"import-maps": "Karten importieren"
},
"link-maps": {
"tooltips": {
@@ -70,6 +71,17 @@
"text": "Deine Karten importieren",
"subtext": "Lasse deine ZIP-Dateien hier fallen, um deine Karten zu importieren"
}
},
"playlists": {
"drop-down": {
"browse-playlists": "Wiedergabelisten durchsuchen",
"create-a-playlist": "Eine Wiedergabeliste erstellen",
"import-playlists": "Wiedergabelisten importieren"
},
"drop-zone": {
"text": "Importiere deine Wiedergabelisten",
"subtext": "Ziehe deine \".bplist\" oder \".json\" Dateien hierhin, um sie zu importieren"
}
}
}
},
@@ -254,11 +266,13 @@
"errors": {
"titles": {
"operation-running": "Vorgang läuft",
"no-internet": "Kein Internet"
"no-internet": "Kein Internet",
"file-not-supported": "Datei wird nicht unterstützt"
},
"msg": {
"operation-running": "Warte auf das Ende des aktuellen Vorgangs und versuche es erneut.",
"no-internet": "Überprüfe deine Verbindung und versuche es erneut."
"no-internet": "Überprüfe deine Verbindung und versuche es erneut.",
"file-not-supported": "Nur {types}-Dateien werden unterstützt."
}
}
},
@@ -519,7 +533,8 @@
"success": "Die Karten wurden erfolgreich importiert.",
"some-success": "Einige Karten wurden erfolgreich importiert.",
"only-accept-zip": "Nur Zip-Dateien werden unterstützt.",
"invalid-zip": "Die Zip-Datei(en) enthalten keine Karten."
"invalid-zip": "Die Zip-Datei(en) enthalten keine Karten.",
"unknown": "Ein unbekannter Fehler ist aufgetreten."
}
}
},
@@ -1083,6 +1098,9 @@
}
}
},
"drop-zone": {
"or-browse-files": "Oder Dateien durchsuchen"
},
"playlist": {
"error-playlist-creation-title": "Fehler beim Erstellen der Playlist",
"error-playlist-creation-desc": "Beim Erstellen der Playlist ist ein Fehler aufgetreten.",
@@ -1197,6 +1215,24 @@
"last-week": "Letzte Woche",
"last-month": "Letzter Monat",
"3-last-month": "Letzte 3 Monate"
},
"playlists-imported": "Wiedergabelisten importiert",
"all-playlists-have-been-successfully-imported": "Alle Wiedergabelisten wurden erfolgreich importiert",
"no-playlist-found": "Keine Wiedergabeliste gefunden",
"no-playlist-found-in-selected-files": "Keine Wiedergabeliste in den ausgewählten Dateien gefunden",
"some-playlists-not-imported": "Einige Wiedergabelisten wurden nicht importiert",
"some-playlists-have-been-imported": {
"INVALID_SOURCE": "Einige Wiedergabelisten konnten nicht gefunden werden",
"INVALID_PLAYLIST_FILE": "Einige Wiedergabelisten sind ungültig",
"CANNOT_PARSE_PLAYLIST": "Einige Wiedergabelisten sind nicht lesbar",
"unknown": "Einige Wiedergabelisten konnten nicht importiert werden"
},
"no-playlists-imported": "Keine Wiedergabelisten importiert",
"no-playlists-imported-errors": {
"INVALID_SOURCE": "Wiedergabelisten konnten nicht gefunden werden",
"INVALID_PLAYLIST_FILE": "Wiedergabelisten sind ungültig",
"CANNOT_PARSE_PLAYLIST": "Wiedergabelisten sind nicht lesbar",
"unknown": "Es konnte keine Wiedergabeliste importiert werden"
}
},
"dateformat": {
+41 -6
View File
@@ -52,8 +52,9 @@
"tabs": {
"maps": {
"actions": {
"add-maps": {
"text": "Add"
"drop-down": {
"browse-maps": "Browse maps",
"import-maps": "Import maps"
},
"link-maps": {
"tooltips": {
@@ -70,7 +71,17 @@
"text": "Import your maps",
"subtext": "Drop your zip files here to import your maps"
}
},
"playlists": {
"drop-down": {
"browse-playlists": "Browse playlists",
"create-a-playlist": "Create a playlist",
"import-playlists": "Import playlists"
},
"drop-zone": {
"text": "Import your playlists",
"subtext": "Drop your \".bplist\" or \".json\" files here to import them"
}
}
}
},
@@ -260,11 +271,13 @@
"errors": {
"titles": {
"operation-running": "Operation running",
"no-internet": "No internet"
"no-internet": "No internet",
"file-not-supported": "File not supported"
},
"msg": {
"operation-running": "Wait for the current operation to finish, then try again.",
"no-internet": "Check your connection and try again."
"no-internet": "Check your connection and try again.",
"file-not-supported": "Only {types} files are supported."
}
}
},
@@ -527,7 +540,8 @@
"success": "Maps successfully imported.",
"some-success": "Some maps were successfully imported.",
"only-accept-zip": "Only zip files are supported.",
"invalid-zip": "The zip file(s) do not contain any maps."
"invalid-zip": "The zip file(s) do not contain any maps.",
"unknown": "An unknown error occurred."
}
}
},
@@ -1091,6 +1105,9 @@
}
}
},
"drop-zone": {
"or-browse-files": "Or browse files"
},
"playlist": {
"error-playlist-creation-title": "Error creating playlist",
"error-playlist-creation-desc": "An error occurred while creating the playlist.",
@@ -1205,6 +1222,24 @@
"last-week": "Last week",
"last-month": "Last month",
"3-last-month": "Last 3 months"
},
"playlists-imported": "Playlists imported",
"all-playlists-have-been-successfully-imported": "All playlists have been successfully imported",
"no-playlist-found": "No playlist found",
"no-playlist-found-in-selected-files": "No playlist found in the selected files",
"some-playlists-not-imported": "Some playlists not imported",
"some-playlists-have-been-imported": {
"INVALID_SOURCE": "Some playlists could not be found",
"INVALID_PLAYLIST_FILE": "Some playlists are invalid",
"CANNOT_PARSE_PLAYLIST": "Some playlists are unreadable",
"unknown": "Some playlists could not be imported"
},
"no-playlists-imported": "No playlists imported",
"no-playlists-imported-errors": {
"INVALID_SOURCE": "Playlists could not be found",
"INVALID_PLAYLIST_FILE": "Playlists are invalid",
"CANNOT_PARSE_PLAYLIST": "Playlists are unreadable",
"unknown": "No playlist could be imported"
}
},
"dateformat": {
+41 -5
View File
@@ -52,8 +52,9 @@
"tabs": {
"maps": {
"actions": {
"add-maps": {
"text": "Añadir"
"drop-down": {
"browse-maps": "Explorar mapas",
"import-maps": "Importar mapas"
},
"link-maps": {
"tooltips": {
@@ -70,6 +71,17 @@
"text": "Importar tus mapas",
"subtext": "Suelta tus archivos zip aquí para importar tus mapas"
}
},
"playlists": {
"drop-down": {
"browse-playlists": "Explorar listas de reproducción",
"create-a-playlist": "Crear una lista de reproducción",
"import-playlists": "Importar listas de reproducción"
},
"drop-zone": {
"text": "Importa tus listas de reproducción",
"subtext": "Suelta tus archivos \".bplist\" o \".json\" aquí para importarlos"
}
}
}
},
@@ -254,11 +266,13 @@
"errors": {
"titles": {
"operation-running": "Operación en curso",
"no-internet": "Sin internet"
"no-internet": "Sin internet",
"file-not-supported": "Archivo no compatible"
},
"msg": {
"operation-running": "Espera a que termine la operación actual y vuelve a empezar.",
"no-internet": "Comprueba tu conexión a Internet e inténtalo de nuevo."
"no-internet": "Comprueba tu conexión a Internet e inténtalo de nuevo.",
"file-not-supported": "Solo se admiten archivos {types}."
}
}
},
@@ -519,7 +533,8 @@
"success": "Los mapas se han importado con éxito.",
"some-success": "Algunos mapas se han importado con éxito.",
"only-accept-zip": "Solo se admiten archivos zip.",
"invalid-zip": "El o los archivos zip no contienen mapas."
"invalid-zip": "El o los archivos zip no contienen mapas.",
"unknown": "Se produjo un error desconocido."
}
}
},
@@ -1083,6 +1098,9 @@
}
}
},
"drop-zone": {
"or-browse-files": "O navegar por los archivos"
},
"playlist": {
"error-playlist-creation-title": "Error al crear la lista de reproducción",
"error-playlist-creation-desc": "Ocurrió un error al crear la lista de reproducción.",
@@ -1197,6 +1215,24 @@
"last-week": "Última semana",
"last-month": "Último mes",
"3-last-month": "Últimos 3 meses"
},
"playlists-imported": "Listas de reproducción importadas",
"all-playlists-have-been-successfully-imported": "Todas las listas de reproducción se han importado con éxito",
"no-playlist-found": "No se encontró ninguna lista de reproducción",
"no-playlist-found-in-selected-files": "No se encontró ninguna lista de reproducción en los archivos seleccionados",
"some-playlists-not-imported": "Algunas listas de reproducción no se importaron",
"some-playlists-have-been-imported": {
"INVALID_SOURCE": "No se pudieron encontrar algunas listas de reproducción",
"INVALID_PLAYLIST_FILE": "Algunas listas de reproducción no son válidas",
"CANNOT_PARSE_PLAYLIST": "Algunas listas de reproducción no se pueden leer",
"unknown": "No se pudieron importar algunas listas de reproducción"
},
"no-playlists-imported": "No se importaron listas de reproducción",
"no-playlists-imported-errors": {
"INVALID_SOURCE": "No se pudieron encontrar las listas de reproducción",
"INVALID_PLAYLIST_FILE": "Las listas de reproducción no son válidas",
"CANNOT_PARSE_PLAYLIST": "Las listas de reproducción no se pueden leer",
"unknown": "No se pudo importar ninguna lista de reproducción"
}
},
"dateformat": {
+42 -5
View File
@@ -52,8 +52,9 @@
"tabs": {
"maps": {
"actions": {
"add-maps": {
"text": "Ajouter"
"drop-down": {
"browse-maps": "Explorer les maps",
"import-maps": "Importer des maps"
},
"link-maps": {
"tooltips": {
@@ -70,6 +71,17 @@
"text": "Importer vos maps",
"subtext": "Déposez vos fichiers zip ici pour importer vos maps"
}
},
"playlists": {
"drop-down": {
"browse-playlists": "Explorer les playlists",
"create-a-playlist": "Créer une playlist",
"import-playlists": "Importer des playlists"
},
"drop-zone": {
"text": "Importer vos playlists",
"subtext": "Déposez vos fichiers \".bplist\" ou \".json\" ici pour les importer"
}
}
}
},
@@ -254,11 +266,13 @@
"errors": {
"titles": {
"operation-running": "Opération en cours",
"no-internet": "Pas d'accès Internet"
"no-internet": "Pas d'accès Internet",
"file-not-supported": "Fichier non supporté"
},
"msg": {
"operation-running": "Attends la fin de l'opération en cours puis recommence.",
"no-internet": "Vérifie ta connexion internet et ressaye."
"no-internet": "Vérifie ta connexion internet et ressaye.",
"file-not-supported": "Seuls les fichiers {types} sont pris en charge."
}
}
},
@@ -519,7 +533,8 @@
"success": "Les maps ont été importées avec succès.",
"some-success": "Certaines maps ont été importées avec succès.",
"only-accept-zip": "Seules les fichiers zip sont pris en charge.",
"invalid-zip": "Le ou les fichiers zip ne contiennent aucune maps."
"invalid-zip": "Le ou les fichiers zip ne contiennent aucune maps.",
"unknown": "Une erreur inconnue s'est produite."
}
}
},
@@ -1085,6 +1100,9 @@
}
}
},
"drop-zone": {
"or-browse-files": "Ou parcourir les fichiers"
},
"playlist": {
"error-playlist-creation-title": "Erreur lors de la création de la playlist",
"error-playlist-creation-desc": "Une erreur est survenue lors de la création de la playlist.",
@@ -1199,7 +1217,26 @@
"last-week": "Dernière semaine",
"last-month": "Dernier mois",
"3-last-month": "3 derniers mois"
},
"playlists-imported": "Playlists importées",
"all-playlists-have-been-successfully-imported": "Toutes les playlists ont été importées avec succès",
"no-playlist-found": "Aucune playlist trouvée",
"no-playlist-found-in-selected-files": "Aucune playlist trouvée dans les fichiers sélectionnés",
"some-playlists-not-imported": "Certaines playlists non importées",
"some-playlists-have-been-imported": {
"INVALID_SOURCE": "Certaines playlists n'ont pas pu être trouvées",
"INVALID_PLAYLIST_FILE": "Certaines playlists ne sont pas valides",
"CANNOT_PARSE_PLAYLIST": "Certaines playlists ne sont pas lisibles",
"unknown": "Certaines playlists n'ont pas pu être importées"
},
"no-playlists-imported": "Aucune playlist importée",
"no-playlists-imported-errors": {
"INVALID_SOURCE": "Les playlists n'ont pas été trouvées",
"INVALID_PLAYLIST_FILE": "Les playlists ne sont pas valides",
"CANNOT_PARSE_PLAYLIST": "Les playlists ne sont pas lisibles",
"unknown": "Aucune playlist n'a pu être importée"
}
},
"dateformat": {
"dayNames": ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"],
+41 -5
View File
@@ -52,8 +52,9 @@
"tabs": {
"maps": {
"actions": {
"add-maps": {
"text": "追加"
"drop-down": {
"browse-maps": "マップを閲覧",
"import-maps": "マップをインポート"
},
"link-maps": {
"tooltips": {
@@ -70,6 +71,17 @@
"text": "マップをインポート",
"subtext": "ZIPファイルをここにドロップしてマップをインポート"
}
},
"playlists": {
"drop-down": {
"browse-playlists": "プレイリストを閲覧",
"create-a-playlist": "プレイリストを作成",
"import-playlists": "プレイリストをインポート"
},
"drop-zone": {
"text": "プレイリストをインポート",
"subtext": "\".bplist\" または \".json\" ファイルをここにドロップしてインポート"
}
}
}
},
@@ -254,11 +266,13 @@
"errors": {
"titles": {
"operation-running": "稼働中",
"no-internet": "インターネット接続がありません"
"no-internet": "インターネット接続がありません",
"file-not-supported": "ファイルはサポートされていません"
},
"msg": {
"operation-running": "現在の処理が終了するのを待ってから、もう一度試してください。",
"no-internet": "インターネット接続を確認し、もう一度お試しください。"
"no-internet": "インターネット接続を確認し、もう一度お試しください。",
"file-not-supported": "{types}ファイルのみがサポートされています。"
}
}
},
@@ -519,7 +533,8 @@
"success": "マップが正常にインポートされました。",
"some-success": "一部のマップが正常にインポートされました。",
"only-accept-zip": "zipファイルのみがサポートされています。",
"invalid-zip": "zipファイルにマップが含まれていません。"
"invalid-zip": "zipファイルにマップが含まれていません。",
"unknown": "不明なエラーが発生しました。"
}
}
},
@@ -1083,6 +1098,9 @@
}
}
},
"drop-zone": {
"or-browse-files": "またはファイルを参照"
},
"playlist": {
"error-playlist-creation-title": "プレイリストの作成エラー",
"error-playlist-creation-desc": "プレイリストの作成中にエラーが発生しました。",
@@ -1197,6 +1215,24 @@
"last-week": "先週",
"last-month": "先月",
"3-last-month": "過去3ヶ月"
},
"playlists-imported": "プレイリストがインポートされました",
"all-playlists-have-been-successfully-imported": "すべてのプレイリストが正常にインポートされました",
"no-playlist-found": "プレイリストが見つかりません",
"no-playlist-found-in-selected-files": "選択されたファイルにプレイリストが見つかりません",
"some-playlists-not-imported": "一部のプレイリストはインポートされませんでした",
"some-playlists-have-been-imported": {
"INVALID_SOURCE": "一部のプレイリストが見つかりませんでした",
"INVALID_PLAYLIST_FILE": "一部のプレイリストは無効です",
"CANNOT_PARSE_PLAYLIST": "一部のプレイリストは読み取れません",
"unknown": "一部のプレイリストをインポートできませんでした"
},
"no-playlists-imported": "プレイリストはインポートされませんでした",
"no-playlists-imported-errors": {
"INVALID_SOURCE": "プレイリストが見つかりませんでした",
"INVALID_PLAYLIST_FILE": "プレイリストは無効です",
"CANNOT_PARSE_PLAYLIST": "プレイリストは読み取れません",
"unknown": "プレイリストをインポートできませんでした"
}
},
"dateformat": {
+41 -5
View File
@@ -52,8 +52,9 @@
"tabs": {
"maps": {
"actions": {
"add-maps": {
"text": "Добавить"
"drop-down": {
"browse-maps": "Просмотр карт",
"import-maps": "Импорт карт"
},
"link-maps": {
"tooltips": {
@@ -70,6 +71,17 @@
"text": "Импортируйте свои карты",
"subtext": "Перетащите сюда файлы ZIP, чтобы импортировать свои карты"
}
},
"playlists": {
"drop-down": {
"browse-playlists": "Просмотр плейлистов",
"create-a-playlist": "Создать плейлист",
"import-playlists": "Импорт плейлистов"
},
"drop-zone": {
"text": "Импортируйте ваши плейлисты",
"subtext": "Перетащите сюда файлы \".bplist\" или \".json\" для их импорта"
}
}
}
},
@@ -254,11 +266,13 @@
"errors": {
"titles": {
"operation-running": "Операция в процессе",
"no-internet": "Нет интернета"
"no-internet": "Нет интернета",
"file-not-supported": "Файл не поддерживается"
},
"msg": {
"operation-running": "Дождитесь завершения операции, потом попробуйте снова.",
"no-internet": "Проверьте своё соединение и попробуйте снова."
"no-internet": "Проверьте своё соединение и попробуйте снова.",
"file-not-supported": "Поддерживаются только файлы {types}."
}
}
},
@@ -519,7 +533,8 @@
"success": "Карты были успешно импортированы.",
"some-success": "Некоторые карты успешно импортированы.",
"only-accept-zip": "Поддерживаются только zip-файлы.",
"invalid-zip": "В zip-файле(ах) нет карт."
"invalid-zip": "В zip-файле(ах) нет карт.",
"unknown": "Произошла неизвестная ошибка."
}
}
},
@@ -1082,6 +1097,9 @@
}
}
},
"drop-zone": {
"or-browse-files": "Или просмотреть файлы"
},
"playlist": {
"error-playlist-creation-title": "Ошибка при создании плейлиста",
"error-playlist-creation-desc": "Произошла ошибка при создании плейлиста.",
@@ -1196,6 +1214,24 @@
"last-week": "Последняя неделя",
"last-month": "Последний месяц",
"3-last-month": "Последние 3 месяца"
},
"playlists-imported": "Плейлисты импортированы",
"all-playlists-have-been-successfully-imported": "Все плейлисты успешно импортированы",
"no-playlist-found": "Плейлист не найден",
"no-playlist-found-in-selected-files": "Плейлист не найден в выбранных файлах",
"some-playlists-not-imported": "Некоторые плейлисты не импортированы",
"some-playlists-have-been-imported": {
"INVALID_SOURCE": "Некоторые плейлисты не найдены",
"INVALID_PLAYLIST_FILE": "Некоторые плейлисты недействительны",
"CANNOT_PARSE_PLAYLIST": "Некоторые плейлисты нечитаемы",
"unknown": "Некоторые плейлисты не удалось импортировать"
},
"no-playlists-imported": "Плейлисты не импортированы",
"no-playlists-imported-errors": {
"INVALID_SOURCE": "Плейлисты не найдены",
"INVALID_PLAYLIST_FILE": "Плейлисты недействительны",
"CANNOT_PARSE_PLAYLIST": "Плейлисты нечитаемы",
"unknown": "Не удалось импортировать плейлисты"
}
},
"dateformat": {
+41 -5
View File
@@ -52,8 +52,9 @@
"tabs": {
"maps": {
"actions": {
"add-maps": {
"text": "新增"
"drop-down": {
"browse-maps": "瀏覽地圖",
"import-maps": "匯入地圖"
},
"link-maps": {
"tooltips": {
@@ -70,6 +71,17 @@
"text": "導入您的地圖",
"subtext": "將ZIP文件拖放到此處以導入您的地圖"
}
},
"playlists": {
"drop-down": {
"browse-playlists": "瀏覽播放列表",
"create-a-playlist": "建立播放列表",
"import-playlists": "匯入播放列表"
},
"drop-zone": {
"text": "匯入您的播放列表",
"subtext": "將您的 \".bplist\" 或 \".json\" 檔案拖曳到這裡進行匯入"
}
}
}
},
@@ -254,11 +266,13 @@
"errors": {
"titles": {
"operation-running": "操作進行中",
"no-internet": "無網路"
"no-internet": "無網路",
"file-not-supported": "文件不受支持"
},
"msg": {
"operation-running": "等待當前操作完成,然後重試。",
"no-internet": "檢查你的連接並重試。"
"no-internet": "檢查你的連接並重試。",
"file-not-supported": "僅支援{types}檔案。"
}
}
},
@@ -519,7 +533,8 @@
"success": "地圖已成功匯入。",
"some-success": "部分地圖已成功匯入。",
"only-accept-zip": "僅支援zip檔案。",
"invalid-zip": "zip檔案中沒有地圖。"
"invalid-zip": "zip檔案中沒有地圖。",
"unknown": "發生了未知錯誤。"
}
}
},
@@ -1083,6 +1098,9 @@
}
}
},
"drop-zone": {
"or-browse-files": "或瀏覽檔案"
},
"playlist": {
"error-playlist-creation-title": "建立播放清單時發生錯誤",
"error-playlist-creation-desc": "建立播放清單時發生錯誤。",
@@ -1197,6 +1215,24 @@
"last-week": "上週",
"last-month": "上個月",
"3-last-month": "最近3個月"
},
"playlists-imported": "播放清單已匯入",
"all-playlists-have-been-successfully-imported": "所有播放清單已成功匯入",
"no-playlist-found": "未找到播放清單",
"no-playlist-found-in-selected-files": "在選定檔案中未找到播放清單",
"some-playlists-not-imported": "部分播放清單未匯入",
"some-playlists-have-been-imported": {
"INVALID_SOURCE": "某些播放清單未找到",
"INVALID_PLAYLIST_FILE": "某些播放清單無效",
"CANNOT_PARSE_PLAYLIST": "某些播放清單無法解析",
"unknown": "某些播放清單無法匯入"
},
"no-playlists-imported": "未匯入任何播放清單",
"no-playlists-imported-errors": {
"INVALID_SOURCE": "播放清單未找到",
"INVALID_PLAYLIST_FILE": "播放清單無效",
"CANNOT_PARSE_PLAYLIST": "播放清單無法解析",
"unknown": "無法匯入任何播放清單"
}
},
"dateformat": {
+41 -5
View File
@@ -52,8 +52,9 @@
"tabs": {
"maps": {
"actions": {
"add-maps": {
"text": "添加"
"drop-down": {
"browse-maps": "浏览地图",
"import-maps": "导入地图"
},
"link-maps": {
"tooltips": {
@@ -70,6 +71,17 @@
"text": "导入您的地图",
"subtext": "将ZIP文件拖放到此处以导入您的地图"
}
},
"playlists": {
"drop-down": {
"browse-playlists": "浏览播放列表",
"create-a-playlist": "创建播放列表",
"import-playlists": "导入播放列表"
},
"drop-zone": {
"text": "导入您的播放列表",
"subtext": "将您的 \".bplist\" 或 \".json\" 文件拖放到这里进行导入"
}
}
}
},
@@ -254,11 +266,13 @@
"errors": {
"titles": {
"operation-running": "操作进行中",
"no-internet": "无网络"
"no-internet": "无网络",
"file-not-supported": "文件不受支持"
},
"msg": {
"operation-running": "等待当前操作完成,然后重试。",
"no-internet": "检查你的连接并重试。"
"no-internet": "检查你的连接并重试。",
"file-not-supported": "仅支持{types}文件。"
}
}
},
@@ -519,7 +533,8 @@
"success": "地图已成功导入。",
"some-success": "部分地图已成功导入。",
"only-accept-zip": "仅支持zip文件。",
"invalid-zip": "zip文件中没有地图。"
"invalid-zip": "zip文件中没有地图。",
"unknown": "发生了未知错误。"
}
}
},
@@ -1083,6 +1098,9 @@
}
}
},
"drop-zone": {
"or-browse-files": "或浏览文件"
},
"playlist": {
"error-playlist-creation-title": "创建播放列表时出错",
"error-playlist-creation-desc": "创建播放列表时发生错误。",
@@ -1197,6 +1215,24 @@
"last-week": "上周",
"last-month": "上个月",
"3-last-month": "最近3个月"
},
"playlists-imported": "播放列表已导入",
"all-playlists-have-been-successfully-imported": "所有播放列表已成功导入",
"no-playlist-found": "未找到播放列表",
"no-playlist-found-in-selected-files": "在选定文件中未找到播放列表",
"some-playlists-not-imported": "部分播放列表未导入",
"some-playlists-have-been-imported": {
"INVALID_SOURCE": "某些播放列表未找到",
"INVALID_PLAYLIST_FILE": "某些播放列表无效",
"CANNOT_PARSE_PLAYLIST": "某些播放列表无法解析",
"unknown": "某些播放列表无法导入"
},
"no-playlists-imported": "未导入任何播放列表",
"no-playlists-imported-errors": {
"INVALID_SOURCE": "播放列表未找到",
"INVALID_PLAYLIST_FILE": "播放列表无效",
"CANNOT_PARSE_PLAYLIST": "播放列表无法解析",
"unknown": "无法导入任何播放列表"
}
},
"dateformat": {
+2
View File
@@ -8,6 +8,7 @@ import crypto from "crypto";
import { execSync } from "child_process";
import { tryit } from "../../shared/helpers/error.helpers";
import { CustomError } from "shared/models/exceptions/custom-error.class";
import { ErrorObject } from "serialize-error";
export async function pathExist(path: string): Promise<boolean> {
try {
@@ -279,4 +280,5 @@ export interface Progression<T = unknown, D = unknown> {
diff?: number;
data?: T;
extra?: D;
lastError?: ErrorObject;
}
+5
View File
@@ -59,6 +59,11 @@ ipc.on("export-playlists", (args, reply) => {
reply(playlists.exportPlaylists(args));
});
ipc.on("import-playlists", (args, reply) => {
const playlists = LocalPlaylistsManagerService.getInstance();
reply(playlists.importPlaylists(args));
});
ipc.on("install-playlist-file", (args, reply) => {
const playlists = LocalPlaylistsManagerService.getInstance();
+5
View File
@@ -13,6 +13,11 @@ ipc.on("new-window", (args, reply) => {
reply(from(shell.openExternal(args)));
});
ipc.on("open-dialog", (args, reply) => {
// Use this ipc instead of "choose-folder" or "choose-file" to have more control over the dialog
reply(from(dialog.showOpenDialog(args)));
})
ipc.on("choose-folder", (args, reply) => {
reply(from(dialog.showOpenDialog({ properties: ["openDirectory"], defaultPath: args ?? "" })));
});
@@ -26,6 +26,7 @@ import { tryit } from "shared/helpers/error.helpers";
import recursiveReadDir from "recursive-readdir";
import { BsvMapDetail, SongDetails } from "shared/models/maps";
import { findHashInString } from "shared/helpers/string.helpers";
import { serializeError } from "serialize-error";
export class LocalPlaylistsManagerService {
private static instance: LocalPlaylistsManagerService;
@@ -97,7 +98,7 @@ export class LocalPlaylistsManagerService {
const dest = await (async () => {
if(opt.dest && path.isAbsolute(opt.dest) && this.acceptPlaylistFiletype(opt.dest)) { return opt.dest; }
const playlistFolder = await this.getPlaylistsFolder(opt.version);
const playlistPath = path.join(playlistFolder, `${sanitize(opt.bpList.playlistTitle)}${path.extname(opt.dest)}`);
const playlistPath = path.join(playlistFolder, `${sanitize(opt.bpList.playlistTitle)}.bplist`);
return getUniqueFileNamePath(playlistPath);
})();
@@ -122,7 +123,7 @@ export class LocalPlaylistsManagerService {
const dest = await (async () => {
if(opt.dest && path.isAbsolute(opt.dest) && this.acceptPlaylistFiletype(opt.dest)) { return opt.dest; }
const playlistFolder = await this.getPlaylistsFolder(opt.version);
return path.join(playlistFolder, `${sanitize(bplist.playlistTitle)}${path.extname(opt.dest)}`);
return path.join(playlistFolder, `${sanitize(bplist.playlistTitle)}.bplist`);
})();
writeFileSync(dest, JSON.stringify(bplist, null, 2));
@@ -137,13 +138,22 @@ export class LocalPlaylistsManagerService {
const isLocalFile = await pathExists(source).catch(e => { log.error(e); return false; });
if(!isLocalFile && !isValidUrl(source)) {
throw new Error(`Invalid source ${source}`);
throw new CustomError(`Invalid source (${source})`, "INVALID_SOURCE");
}
const bpList: BPList = isLocalFile ? JSON.parse(readFileSync(source).toString()) : await this.request.getJSON<BPList>(source);
const bpList: BPList = await (async () => {
if(isLocalFile){
const res = await tryit(async () => JSON.parse(readFileSync(source).toString()));
if(res.error) {
throw CustomError.fromError(res.error, "CANNOT_PARSE_PLAYLIST");
}
return res.result;
}
return this.request.getJSON<BPList>(source);
})();
if(!bpList?.playlistTitle) {
throw new Error(`Invalid playlist file ${source}`);
throw new CustomError(`Invalid playlist file (${source})`, "INVALID_PLAYLIST_FILE");
}
bpList.songs = (bpList.songs ?? []).map(s => s.hash ? (
@@ -430,6 +440,49 @@ export class LocalPlaylistsManagerService {
return archive.finalize();
}
public importPlaylists({ version, paths }: { version?: BSVersion, paths: string[] }): Observable<Progression<LocalBPListsDetails>> {
return new Observable<Progression<LocalBPListsDetails>>(obs => {
let canceled = false;
(async () => {
const bplistPaths = paths.filter(p => this.acceptPlaylistFiletype(p));
const progress: Progression<LocalBPListsDetails> = { current: 0, total: bplistPaths.length, data: null };
obs.next(progress);
for(const playlistPath of bplistPaths) {
if(canceled) {
log.info("Playlist import canceled");
return;
}
const { result: localBPList, error } = await tryit(() => this.installBPListFile({ bplistSource: playlistPath, version }));
if(error) {
progress.lastError = serializeError(CustomError.fromError(error));
obs.next(progress);
log.error(error);
continue;
}
const bpListDetails = this.getLocalBPListDetails(localBPList.localBPList);
progress.current += 1;
progress.data = bpListDetails;
obs.next(progress);
}
})()
.catch(err => obs.error(err))
.finally(() => obs.complete());
return () => {
canceled = true;
}
});
}
public oneClickInstallPlaylist(bplistUrl: string): Observable<Progression<DownloadPlaylistProgressionData>> {
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
@@ -348,14 +348,15 @@ export class LocalMapsManagerService {
const zip = new StreamZip.async({ file: zipPath });
const { result: zipEntries, error } = await tryit(() => zip.entries());
const zipEntriesValues = Object.values(zipEntries);
if(error) {
log.error("Could not read zip entries", zipPath, error);
await zip.close();
const res = await tryit(() => zip.close());
log.error("Could not read zip entries", zipPath, error, res?.error);
continue;
}
const zipEntriesValues = Object.values(zipEntries);
const mapsFolders = zipEntriesValues.reduce((acc, entry) => {
if(!/(^|\/)[Ii]nfo\.dat$/.test(entry.name)){ return acc; }
acc.push(path.dirname(entry.name));
@@ -10,7 +10,6 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
import { useService } from "renderer/hooks/use-service.hook";
import { BsContentTabPanel } from "../shared/bs-content-tab-panel/bs-content-tab-panel.component";
import { BsmButton } from "../shared/bsm-button.component";
import { MapIcon } from "../svgs/icons/map-icon.component";
import { PlaylistIcon } from "../svgs/icons/playlist-icon.component";
import { useObservable } from "renderer/hooks/use-observable.hook";
@@ -24,7 +23,6 @@ import { PlaylistDownloaderService } from "renderer/services/playlist-downloader
import { LocalPlaylistFilter, LocalPlaylistFilterPanel } from "./playlists/local-playlist-filter-panel.component";
import { noop } from "shared/helpers/function.helpers";
import { Dropzone } from "../shared/dropzone.component";
import { NotificationService } from "renderer/services/notification.service";
import { logRenderError } from "renderer";
type Props = {
@@ -45,11 +43,13 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
const mapsDownloader = useService(MapsDownloaderService);
const playlistsManager = useService(PlaylistsManagerService);
const playlistsDownloader = useService(PlaylistDownloaderService);
const notifications = useService(NotificationService);
const t = useTranslation();
const [tabIndex, setTabIndex] = useState(0);
const [mapsDropZoneOpen, setMapsDropZoneOpen] = useState(false);
const [playlistsDropZoneOpen, setPlaylistsDropZoneOpen] = useState(false);
const maps$ = useConstant(() => new BehaviorSubject<BsmLocalMap[]>(undefined));
const playlists$ = useConstant(() => new BehaviorSubject<LocalBPListsDetails[]>(undefined));
@@ -77,11 +77,16 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
return playlistsManager.$playlistsFolderLinkState(version);
}, FolderLinkState.Unlinked, [version]);
const handleAddClick = () => {
const handleBrowse = () => {
switch (tabIndex) {
case 0: return mapsDownloader.openDownloadMapModal(version, maps$.value);
case 1: return playlistsDownloader.openDownloadPlaylistModal(version, playlists$, maps$);
default: return noop();
case 0:
mapsDownloader.openDownloadMapModal(version, maps$.value);
return;
case 1:
playlistsDownloader.openDownloadPlaylistModal(version, playlists$, maps$);
return;
default:
return noop();
}
}
@@ -105,26 +110,30 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
return playlistsManager.unlinkVersion(version);
}
const handleFileDrop = async (files: FileList) => {
const zipMimeTypes = ["application/zip", "application/zip-compressed", "application/x-zip-compressed"];
const paths: string[] = Array.from(files).reduce((acc, file) => {
if (zipMimeTypes.includes(file.type)) {
acc.push(window.electron.webUtils.getPathForFile(file));
}
return acc;
}, []);
const importMaps = async (paths: string[]) => {
setMapsDropZoneOpen(() => false);
return lastValueFrom(mapsManager.importMaps(paths, version)).catch(logRenderError);
}
if (paths.length === 0) {
notifications.notifyError({
title: "notifications.maps.import-map.titles.error",
desc: "notifications.maps.import-map.msgs.only-accept-zip"
});
return;
const importPlaylists = async (paths: string[]) => {
setPlaylistsDropZoneOpen(() => false);
return lastValueFrom(playlistsManager.importPlaylists({ version, paths })).catch(logRenderError);
}
const addDropDownItems = ((): DropDownItem[] => {
if(tabIndex === 0){
return [
{ icon: "browse", text: "pages.version-viewer.maps.tabs.maps.actions.drop-down.browse-maps", onClick: handleBrowse },
{ icon: "download", text: "pages.version-viewer.maps.tabs.maps.actions.drop-down.import-maps", onClick: () => setMapsDropZoneOpen(true) }
];
}
const import$ = mapsManager.importMaps(paths, version);
return lastValueFrom(import$).catch(logRenderError);
}
return [
{ icon: "browse", text: "pages.version-viewer.maps.tabs.playlists.drop-down.browse-playlists", onClick: handleBrowse },
{ icon: "add-file", text: "pages.version-viewer.maps.tabs.playlists.drop-down.create-a-playlist", onClick: () => playlistsRef?.current?.createPlaylist?.() },
{ icon: "download", text: "pages.version-viewer.maps.tabs.playlists.drop-down.import-playlists", onClick: () => setPlaylistsDropZoneOpen(true) }
];
})();
const dropDownItems = ((): DropDownItem[] => {
if (tabIndex === 0) {
@@ -136,7 +145,6 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
}
return [
{ icon: "add", text: t("playlist.create-a-playlist"), onClick: () => playlistsRef?.current?.createPlaylist?.() },
{ icon: "sync", text: t("playlist.synchronize-playlists"), onClick: () => playlistsRef?.current?.syncPlaylists?.() },
{ icon: "export", text: t("playlist.export-playlists"), onClick: () => playlistsRef?.current?.exportPlaylists?.() },
{ icon: "trash", text: t("playlist.delete-playlists"), onClick: () => playlistsRef?.current?.deletePlaylists?.() },
@@ -146,13 +154,20 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
return (
<>
<nav className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 text-main-color-1 dark:text-white mb-4">
<BsmButton
className="flex items-center justify-center w-fit rounded-full px-2 py-1 font-bold whitespace-nowrap"
<BsmDropdownButton
classNames={{
mainContainer: "h-full relative z-[1] flex justify-center w-fit",
button: "flex items-center justify-center h-full rounded-full px-2 py-1 font-bold whitespace-nowrap shadow-none",
itemsContainer: "bg-theme-3",
}}
textClassName="whitespace-nowrap"
icon="add"
buttonColor="primary"
text="misc.add"
typeColor="primary"
align="center"
menuTranslationY="6px"
withBar={false}
onClick={handleAddClick}
items={addDropDownItems}
/>
<div className="h-full rounded-full bg-light-main-color-2 dark:bg-main-color-2 grow p-[6px]">
<input
@@ -200,18 +215,37 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
]}
>
<InstalledMapsContext.Provider value={mapsContextValue}>
<Dropzone
className="w-full h-full shrink-0"
onDrop={event => {
handleFileDrop(event.dataTransfer.files);
}}
onFiles={importMaps}
text={t("pages.version-viewer.maps.tabs.maps.drop-zone.text")}
subtext={t("pages.version-viewer.maps.tabs.maps.drop-zone.subtext")}
open={mapsDropZoneOpen}
onClose={mapsDropZoneOpen ? () => setMapsDropZoneOpen(() => false) : undefined}
filters={[{ name: ".zip", extensions: ["zip"] }]}
dialogOptions={{ dialog: {
properties: ["openFile", "multiSelections"],
}}}
>
<LocalMapsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 0} ref={mapsRef} version={version} filter={mapFilter} search={search} linkedState={mapsLinkedState} />
</Dropzone>
<LocalPlaylistsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 1} ref={playlistsRef} version={version} linkedState={playlistLinkedState} filter={playlistFilter} search={search}/>
<Dropzone
className="w-full h-full shrink-0"
onFiles={importPlaylists}
text={t("pages.version-viewer.maps.tabs.playlists.drop-zone.text")}
subtext={t("pages.version-viewer.maps.tabs.playlists.drop-zone.subtext")}
open={playlistsDropZoneOpen}
onClose={playlistsDropZoneOpen ? () => setPlaylistsDropZoneOpen(() => false) : undefined}
filters={[{ name: ".bplist, .json", extensions: ["bplist", "json"] }]}
dialogOptions={{ dialog: {
properties: ["openFile", "multiSelections"],
}}}
>
<LocalPlaylistsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 1} ref={playlistsRef} version={version} linkedState={playlistLinkedState} filter={playlistFilter} search={search}/>
</Dropzone>
</InstalledMapsContext.Provider>
</BsContentTabPanel>
</>
@@ -6,7 +6,7 @@ import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
import { useService } from "renderer/hooks/use-service.hook";
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
import { BehaviorSubject, Observable, combineLatest, distinctUntilChanged, filter, finalize, lastValueFrom, map, tap } from "rxjs";
import { BehaviorSubject, Observable, Subscription, bufferTime, combineLatest, distinctUntilChanged, filter, finalize, lastValueFrom, map, tap } from "rxjs";
import { BSVersion } from "shared/bs-version.interface";
import { noop } from "shared/helpers/function.helpers";
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
@@ -52,7 +52,7 @@ type Props = {
export type LocalPlaylistsListRef = {
createPlaylist: () => Promise<void>;
syncPlaylists: () => Promise<void>;
syncPlaylists: (playlists?: LocalBPList[]) => Promise<void>;
deletePlaylists: () => Promise<void>;
exportPlaylists: () => Promise<void>;
}
@@ -111,9 +111,9 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
}
},
syncPlaylists: async () => {
syncPlaylists: async (playlists?: LocalBPList[]) => {
if(!isOnline){ return; }
const toSync = selectedPlaylists$.value?.length ? selectedPlaylists$.value : playlists$.value;
const toSync = playlists ?? (selectedPlaylists$.value?.length ? selectedPlaylists$.value : playlists$.value);
if(!toSync.length){ return; }
const modalRes = await modals.openModal(SyncPlaylistModal, { data: toSync });
@@ -211,10 +211,22 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
setPlaylists(newPlaylist);
}
const sub = playlistDownloader.currentDownload$.pipe(filter(download => download?.downloaded && equal(download?.info.version, version))).subscribe(download => onPlaylistDownloadedCB(download.downloaded));
const subs: Subscription[] = [];
subs.push(playlistDownloader.currentDownload$.pipe(
filter(download => download?.downloaded && equal(download?.info.version, version))
).subscribe(download => onPlaylistDownloadedCB(download.downloaded)));
subs.push(playlistService.$onPlaylistImported(version).pipe(bufferTime(500)).subscribe({
next: newPlaylists => {
if(!newPlaylists.length){ return; }
const playlists = (playlists$.value ?? []).filter(p => !newPlaylists.some(np => np.path === p.path));
setPlaylists([...newPlaylists, ...playlists]);
}
}));
return () => {
sub.unsubscribe();
subs.forEach(s => s.unsubscribe());
}
}, [isActiveOnce, version, linked]);
@@ -1,38 +1,96 @@
import { AnimatePresence, motion } from "framer-motion";
import { DragEvent, ReactNode, useCallback, useState } from "react";
import { DragEvent, ReactNode, useCallback, useMemo, useState } from "react";
import { BsmImage } from "./bsm-image.component";
import BeatImpatient from "../../../../assets/images/apngs/beat-impatient.png";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import Color from "color";
import { cn } from "renderer/helpers/css-class.helpers";
import { CloseIcon } from "../svgs/icons/close-icon.component";
import { OpenDialogOptions } from "electron";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { useService } from "renderer/hooks/use-service.hook";
import { IpcService } from "renderer/services/ipc.service";
import { NotificationService } from "renderer/services/notification.service";
import { lastValueFrom } from "rxjs";
type Props = Readonly<{
children?: ReactNode;
className?: string;
open?: boolean;
text?: string;
subtext?: string;
onDrop?: (event: DragEvent<HTMLDivElement>) => void;
onFiles?: (paths: string[]) => void;
onClose?: () => void;
filters?: OpenDialogOptions["filters"];
dialogOptions?: {
text?: string;
dialog?: Exclude<OpenDialogOptions, "filters">;
}
}>;
// Supports drop and drop functionality for files.
export function Dropzone({
children,
className,
open,
text,
subtext,
onDrop,
filters,
dialogOptions,
onFiles,
onClose
}: Props) {
const ipc = useService(IpcService);
const notification = useService(NotificationService);
const t = useTranslation();
const [dragCount, setDragCount] = useState(0);
const [dropZoneDragging, setDropZoneDragging] = useState(false);
const themeColor = useThemeColor("first-color");
const color = new Color(themeColor).lighten(.25).saturate(.8).hex();
const supportedExtensions = useMemo(() => {
if(!filters?.length) {
return [];
}
return filters.flatMap(filter => filter.extensions ?? []);
}, [filters]);
const handleDrop = useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
setDragCount(0);
if (onDrop) onDrop(event);
}, []);
setDragCount(() => 0);
setDropZoneDragging(() => false);
if (!onFiles || !event.dataTransfer?.files?.length) {
return;
}
const paths = Array.from(event.dataTransfer.files).reduce<string[]>((acc, file) => {
const path = window.electron.webUtils.getPathForFile(file);
if(supportedExtensions.some(ext => path.endsWith(ext))) {
acc.push(path);
}
return acc;
}, []);
if(!paths.length) {
notification.notifyError({
title: t("notifications.shared.errors.titles.file-not-supported"),
desc: t("notifications.shared.errors.msg.file-not-supported", { types: supportedExtensions.join(", ") }),
duration: 8000
});
return;
}
onFiles(paths);
}, [supportedExtensions]);
const onDragOver = useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
@@ -51,14 +109,33 @@ export function Dropzone({
setDragCount(prev => prev - 1);
}, []);
const openDialog = useCallback(async () => {
if(!dialogOptions?.dialog) {
return;
}
const res = await lastValueFrom(ipc.sendV2("open-dialog", { ...dialogOptions.dialog, filters }));
if(res?.canceled || !res?.filePaths?.length) {
return;
}
onFiles?.(res.filePaths);
}, []);
return (
<div className={className} onDragEnter={onDragEnter} onDragLeave={onDragLeave}>
<div className={cn("relative", className)} onDragEnter={onDragEnter} onDragLeave={onDragLeave}>
<AnimatePresence>
{dragCount ? (
{(dragCount || open) ? (
<div onDragEnter={onDragEnter} onDragLeave={onDragLeave} onDrop={onDragLeave} className="absolute size-full top-0 right-0 z-10">
<motion.div className="absolute size-full top-0 right-0 bg-black" initial={{ opacity: 0 }} animate={{ opacity: .6 }} exit={{ opacity: 0 }}/>
<motion.div className="absolute size-full top-0 right-0 flex justify-center items-center p-5" initial={{ y: "100%" }} animate={{ y: "0" }} exit={{ y: "100%" }}>
<div className="h-72 max-h-full w-full max-w-lg sm:h-96 sm:max-w-2xl bg-theme-3 rounded-md shadow-black shadow-lg p-7">
<div className="relative h-72 max-h-full w-full max-w-lg sm:h-96 sm:max-w-2xl bg-theme-3 rounded-md shadow-black shadow-lg p-7">
{onClose && (
<button className="absolute top-0.5 right-0.5 size-7" onClick={onClose}>
<CloseIcon className="size-full"/>
</button>
)}
<div
onDragOver={onDragOver}
onDrop={handleDrop}
@@ -77,6 +154,9 @@ export function Dropzone({
<BsmImage className="size-28 pointer-events-none" image={BeatImpatient}/>
<span className="text-2xl font-bold pointer-events-none">{text}</span>
<span className="pointer-events-none">{subtext}</span>
{dialogOptions?.dialog && (
<button className="underline hover:no-underline" onClick={openDialog}>{dialogOptions?.text ?? t("drop-zone.or-browse-files")}</button>
)}
</div>
</div>
</motion.div>
@@ -62,9 +62,11 @@ import { ShortcutIcon } from "./icons/shortcut-icon.component";
import { BackupRestoreIcon } from "./icons/backup-restore-icon.component";
import { SongDetailDiffCharactertistic } from "shared/models/maps/song-details-cache/song-details-cache.model";
import { CleanIcon } from "./icons/clean-icon.component";
import { BrowseIcon } from "./icons/browse-icon.component";
import { AddFileIcon } from "./icons/add-file-icon.component";
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag");
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag");
export const BsmIcon = memo(({ className, icon, style }: { className?: string; icon: BsmIconType; style?: CSSProperties }) => {
// TODO : Very ugly very messy, need to find a better way to do this
@@ -262,6 +264,14 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
return <CleanIcon className={className} style={style} />;
}
if (icon === "browse") {
return <BrowseIcon className={className} style={style} />;
}
if(icon === "add-file") {
return <AddFileIcon className={className} style={style} />;
}
return <TrashIcon className={className} style={style} />;
@@ -0,0 +1,9 @@
import { createSvgIcon } from "../svg-icon.type";
export const AddFileIcon = createSvgIcon(( props, ref ) => {
return (
<svg ref={ref} {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
<path d="M440-357.37v80q0 17 11.5 28.5t28.5 11.5q17 0 28.5-11.5t11.5-28.5v-80h80q17 0 28.5-11.5t11.5-28.5q0-17-11.5-28.5t-28.5-11.5h-80v-80q0-17-11.5-28.5t-28.5-11.5q-17 0-28.5 11.5t-11.5 28.5v80h-80q-17 0-28.5 11.5t-11.5 28.5q0 17 11.5 28.5t28.5 11.5h80ZM242.87-71.87q-37.78 0-64.39-26.61t-26.61-64.39v-634.26q0-37.78 26.61-64.39t64.39-26.61H525.8q18.22 0 34.72 6.84 16.5 6.83 29.18 19.51L781.78-669.7q12.68 12.68 19.51 29.18 6.84 16.5 6.84 34.72v442.93q0 37.78-26.61 64.39t-64.39 26.61H242.87Zm274.26-570.76v-154.5H242.87v634.26h474.26v-434.26h-154.5q-19.15 0-32.33-13.17-13.17-13.18-13.17-32.33Zm-274.26-154.5V-597.13v-200 634.26-634.26Z"/>
</svg>
);
})
@@ -0,0 +1,11 @@
import { createSvgIcon } from "../svg-icon.type";
export const BrowseIcon = createSvgIcon(( props, ref ) => {
return (
<svg ref={ref} {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
<path d="M73.38-480q0-84.36 32.02-158.55 32.02-74.2 87.06-129.15 55.03-54.96 129.23-87.06 74.19-32.1 158.55-32.1 134.3 0 238.05 76.51t145.17 196.84q5.8 15.77-1.12 30.78-6.93 15.01-22.85 19.75-14.93 4.2-28.19-4.11-13.25-8.3-18.95-22.99-22.75-67.42-71.58-119.41-48.83-52-116.66-79.69v17.01q0 34.1-23.83 59.02-23.84 24.93-57.93 24.93h-83.87v84.11q0 16.64-12.84 27.87-12.83 11.23-29.75 11.23h-81.28v84.53h60.85q16 0 26.94 11.02 10.93 11.02 10.93 27.02v84.42h-57.2L157.96-556.35q-4.33 19.26-6.5 38.13-2.16 18.87-2.16 37.74 0 122.43 77.84 214.51 77.85 92.08 197.26 111.19 14.87 2.74 23.8 14.97 8.92 12.22 7.57 27.5-1.6 15.83-13.52 25.68-11.92 9.84-27.64 6.72-145.92-23.35-243.57-135.47Q73.38-327.5 73.38-480Zm749.37 355.88L717.04-229.17q-20.76 13.44-44.66 21.04-23.89 7.6-49.14 7.6-73.46 0-125.14-51.53-51.67-51.54-51.67-125.07 0-73.37 51.61-124.79 51.62-51.41 124.99-51.41 73.53 0 124.94 51.55 51.42 51.56 51.42 125.02 0 25.4-7.93 49.26-7.94 23.86-20.71 45.28l105.13 105.05q11.26 11.03 11.47 26.53.22 15.5-10.81 26.52-11.1 11.26-26.89 11.26-15.79 0-26.9-11.26ZM622.86-276.28q42.13 0 71.37-29.27 29.25-29.28 29.25-71.41 0-42.13-29.15-71.37-29.16-29.25-71.29-29.25-42.13 0-71.41 29.15-29.29 29.16-29.29 71.29 0 42.13 29.19 71.49 29.2 29.37 71.33 29.37Z"/>
</svg>
)
});
@@ -242,7 +242,7 @@ export class MapsManagerService {
title: "notifications.maps.import-map.titles.error",
desc: ["invalid-zip"].includes(err?.code)
? `notifications.maps.import-map.msgs.${err.code}`
: "misc.unknown"
: "notifications.maps.import-map.msgs.unknown"
});
obs.error(err);
})
@@ -1,6 +1,6 @@
import { BSVersion } from "shared/bs-version.interface";
import { IpcService } from "./ipc.service";
import { Observable, lastValueFrom } from "rxjs";
import { Observable, Subject, Subscription, catchError, distinctUntilChanged, filter, lastValueFrom, map, of } from "rxjs";
import { FolderLinkState, VersionFolderLinkerService } from "./version-folder-linker.service";
import { Progression } from "main/helpers/fs.helpers";
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
@@ -8,6 +8,11 @@ import { ModalExitCode, ModalService } from "./modale.service";
import { UnlinkPlaylistModal } from "renderer/components/modal/modal-types/unlink-playlist-modal.component";
import { LinkPlaylistModal } from "renderer/components/modal/modal-types/link-playlist-modal.component";
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
import { ProgressBarService } from "./progress-bar.service";
import equal from "fast-deep-equal";
import { noop } from "shared/helpers/function.helpers";
import { NotificationService } from "./notification.service";
import { CustomError } from "shared/models/exceptions/custom-error.class";
export class PlaylistsManagerService {
private static instance: PlaylistsManagerService;
@@ -21,14 +26,20 @@ export class PlaylistsManagerService {
public static readonly RELATIVE_PLAYLISTS_FOLDER = "Playlists";
private readonly lastPlaylistImported$ = new Subject<{ version: BSVersion, playlist: LocalBPListsDetails }>();
private readonly ipc: IpcService;
private readonly linker: VersionFolderLinkerService;
private readonly modal: ModalService;
private readonly progressBar: ProgressBarService;
private readonly notification: NotificationService;
private constructor() {
this.ipc = IpcService.getInstance();
this.linker = VersionFolderLinkerService.getInstance();
this.modal = ModalService.getInstance();
this.progressBar = ProgressBarService.getInstance();
this.notification = NotificationService.getInstance();
}
public getVersionPlaylistsDetails(version: BSVersion): Observable<Progression<LocalBPListsDetails[]>> {
@@ -48,6 +59,69 @@ export class PlaylistsManagerService {
});
}
public importPlaylists(opt: { version?: BSVersion, paths: string[] }): Observable<Progression<LocalBPListsDetails>> {
return new Observable<Progression<LocalBPListsDetails>>(obs => {
if(!this.progressBar.require()){
obs.error("A progression is already running");
return noop;
}
const subs: Subscription[] = [];
const import$ = this.ipc.sendV2("import-playlists", opt);
subs.push(import$.subscribe(obs));
subs.push(import$.pipe(catchError(() => of()), map(progress => progress?.data), filter(Boolean), distinctUntilChanged((a, b) => equal(a, b))).subscribe({
next: playlist => {
this.lastPlaylistImported$.next({ version: opt.version, playlist });
}
}));
this.progressBar.show(import$);
const knownErrors = ["INVALID_SOURCE", "INVALID_PLAYLIST_FILE", "CANNOT_PARSE_PLAYLIST"];
lastValueFrom(import$).then(res => {
if(res.lastError && !res.current){
throw res.lastError;
}
if(!res.total){
return this.notification.notifyWarning({
title: "playlist.no-playlist-found",
desc: "playlist.no-playlist-found-in-selected-files"
});
}
if(res.current < res.total){
return this.notification.notifyWarning({
title: "playlist.some-playlists-not-imported",
desc: knownErrors.includes(res.lastError?.code) ? `playlist.some-playlists-have-been-imported.${res.lastError.code}` : "playlist.some-playlists-have-been-imported.unknown"
});
}
return this.notification.notifySuccess({
title: "playlist.playlists-imported",
desc: "playlist.all-playlists-have-been-successfully-imported",
});
}).catch((err: CustomError) => {
this.notification.notifyError({
title: "playlist.no-playlists-imported",
desc: knownErrors.includes(err.code) ? `playlist.no-playlists-imported-errors.${err.code}` : "playlist.no-playlists-imported-errors.unknown"
});
}).finally(() => {
obs.complete();
})
return () => {
subs.forEach(s => s.unsubscribe());
this.progressBar.hide();
}
});
}
public async linkVersion(version: BSVersion): Promise<boolean> {
const modalRes = await this.modal.openModal(LinkPlaylistModal);
@@ -91,4 +165,11 @@ export class PlaylistsManagerService {
public $playlistsFolderLinkState(version: BSVersion): Observable<FolderLinkState> {
return this.linker.$folderLinkedState(version, PlaylistsManagerService.RELATIVE_PLAYLISTS_FOLDER);
}
public $onPlaylistImported(version?: BSVersion): Observable<LocalBPListsDetails> {
return this.lastPlaylistImported$.pipe(
filter((val) => val?.playlist && equal(val.version, version)),
map((val) => val.playlist)
);
}
}
+4
View File
@@ -53,6 +53,10 @@ export default function App() {
const appVersion = await lastValueFrom(autoUpdater.getAppVersion());
const lastAppVersion = autoUpdater.getLastAppVersion();
if(!lastAppVersion) {
return;
}
if (lastAppVersion.toLowerCase().includes("alpha")) {
autoUpdater.setLastAppVersion("0"); // Reset last app version if it's an alpha version for users with alpha versions in the var stored.
}
@@ -1,17 +1,14 @@
export class CustomError extends Error {
private readonly _code: string;
private readonly _data?: unknown;
public readonly code: string;
public readonly data?: unknown;
constructor(message: string, code: string, data?: unknown){
super(message);
this._code = code;
this._data = data;
this.code = code;
this.data = data;
}
public get code(): string { return this._code; }
public get data(): unknown { return this._data; }
public static fromError(error: Error, code?: string, data?: unknown): CustomError{
if(error instanceof CustomError){
@@ -33,4 +30,4 @@ export class CustomError extends Error {
export enum CustomErrorCodes {
UNKNOWN_ERROR = "UNKNOWN_ERROR",
}
}
+3 -1
View File
@@ -14,7 +14,7 @@ import { BsmLocalModel } from "../models/bsm-local-model.interface";
import { Mod } from "../mods";
import { BPList, DownloadPlaylistProgressionData } from "../playlists/playlist.interface";
import { VersionLinkerAction } from "renderer/services/version-folder-linker.service";
import { FileFilter, OpenDialogReturnValue } from "electron";
import { FileFilter, OpenDialogOptions, OpenDialogReturnValue } from "electron";
import { SystemNotificationOptions } from "../notification/system-notification.model";
import { Supporter } from "../supporters";
import { AppWindow } from "../window-manager/app-window.model";
@@ -93,6 +93,7 @@ export interface IpcChannelMapping {
"get-version-playlists-details": {request: BSVersion, response: Progression<LocalBPListsDetails[]>};
"delete-playlist": {request: {version: BSVersion, bpList: LocalBPList, deleteMaps?: boolean}, response: Progression};
"export-playlists": {request: {version?: BSVersion, bpLists: LocalBPList[], dest: string, playlistsMaps?: BsmLocalMap[]}, response: Progression<string>};
"import-playlists": {request: {version?: BSVersion, paths: string[]}, response: Progression<LocalBPListsDetails>};
"install-playlist-file": {request: {bplist: BPList, version?: BSVersion, dest?: string}, response: LocalBPListsDetails};
/* ** bs-uninstall-ipcs ** */
@@ -122,6 +123,7 @@ export interface IpcChannelMapping {
/* ** os-controls-ipcs ** */
"new-window": { request: string, response: void };
"open-dialog": { request: OpenDialogOptions, response: OpenDialogReturnValue };
"choose-folder": { request: string, response: OpenDialogReturnValue };
"choose-file": { request: string, response: OpenDialogReturnValue }
"choose-image": { request: { multiple?: boolean, base64?: boolean }, response: string[] }