[feature-107] translations

This commit is contained in:
MathieuG-P
2024-06-30 16:03:06 +02:00
parent 05a4386479
commit 65bf528a11
29 changed files with 1087 additions and 154 deletions
@@ -108,10 +108,10 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
}
return [
{ icon: "add", text: "Créer une playlist", onClick: () => playlistsRef?.current?.createPlaylist?.() },
{ icon: "sync", text: "Synchroniser les playlists", onClick: () => playlistsRef?.current?.syncPlaylists?.() },
{ icon: "export", text: "Exporter les playlists", onClick: () => playlistsRef?.current?.exportPlaylists?.() },
{ icon: "trash", text: "Supprimer les playlists", onClick: () => playlistsRef?.current?.deletePlaylists?.() },
{ 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?.() },
];
})();
@@ -119,7 +119,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
<nav className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 text-main-color-1 dark:text-white">
<BsmButton
className="flex items-center justify-center w-fit rounded-full px-2 py-1 font-bold"
className="flex items-center justify-center w-fit rounded-full px-2 py-1 font-bold whitespace-nowrap"
icon="add"
text="misc.add"
typeColor="primary"
@@ -130,13 +130,13 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
<input
type="text"
className="h-full w-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2"
placeholder={tabIndex === 0 ? t("pages.version-viewer.maps.search-bar.search-placeholder") : "Rechercher une playlist"}
placeholder={tabIndex === 0 ? t("pages.version-viewer.maps.search-bar.search-placeholder") : t("playlist.search-playlist")}
value={search}
onChange={e => setSearch(() => e.target.value)}
tabIndex={-1}
/>
</div>
<BsmDropdownButton className="h-full relative z-[1] flex justify-center" buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" textClassName="whitespace-nowrap" withBar={false}>
<BsmDropdownButton className="h-full relative z-[1] flex justify-center" buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1 whitespace-nowrap" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" textClassName="whitespace-nowrap" withBar={false}>
{(
tabIndex === 0 ? (
<FilterPanel className="absolute top-[calc(100%+3px)] origin-top w-[500px] h-fit p-2 rounded-md shadow-md shadow-black" filter={mapFilter} onChange={setMapFilter} />
@@ -210,7 +210,7 @@ export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor,
{diffSet.map(({ name, libelle, stars }, index) => (
<li key={`${name}${libelle}${stars}${index}`} className="w-full h-4 flex items-center gap-1">
{onHighlightedDiffsChange && (
<Tippy content="Surligner la difficulté" placement="top" theme="default">
<Tippy content={t("maps.map-item.hightlight-difficulty")} placement="top" theme="default">
<div className="h-full aspect-square">
<BsmCheckbox className="size-full relative" checked={isDiffHightlighted({name, characteristic: charac})} onChange={() => handleDiffCheckChange({name, characteristic: charac})} />
</div>
@@ -251,7 +251,7 @@ export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor,
<BsmLink className="hover:underline" href={mapUrl}>
{title}
</BsmLink> ) : (
<h1>{title}</h1>
title
)}
</h1>
<h2 className="font-bold whitespace-nowrap text-ellipsis overflow-hidden w-full text-sm mb-[3px]">{songAutor && t("maps.map-item.by", { songAutor })}</h2>
@@ -91,19 +91,19 @@ export function LocalPlaylistFilterPanel({ className, filter, onChange }: Props)
<motion.div className={cn("bg-theme-3 flex flex-col gap-1.5 p-2 absolute origin-top shadow-md shadow-black rounded-md", className)} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{duration: .1}}>
<div className="w-full flex flex-col justify-center p-2">
<BsmRange values={[minNbMaps ?? MIN_NB_MAPS, maxNbMaps ?? MAX_NB_MAPS]} min={MIN_NB_MAPS} max={MAX_NB_MAPS} step={1} onChange={handleOnNbMapsChange} renderLabel={v => renderSimpleMinMaxLabel(v, MAX_NB_MAPS)}/>
<span className=" text-sm font-bold text-center mt-2.5">Nombre de maps</span>
<span className=" text-sm font-bold text-center mt-2.5">{t("playlist.nb-maps")}</span>
</div>
<div className="w-full flex flex-col justify-center p-2">
<BsmRange values={[minNbMappers ?? MIN_NB_MAPPER, maxNbMappers ?? MAX_NB_MAPPER]} min={MIN_NB_MAPPER} max={MAX_NB_MAPPER} step={1} onChange={handleOnNbMapperChange} renderLabel={v => renderSimpleMinMaxLabel(v, MAX_NB_MAPPER)}/>
<span className=" text-sm font-bold text-center mt-2.5">Nombre de mappeurs</span>
<span className=" text-sm font-bold text-center mt-2.5">{t("playlist.nb-mappers")}</span>
</div>
<div className="w-full flex flex-col justify-center p-2">
<BsmRange values={[minDuration ?? MIN_DURATION, maxDuration ?? MAX_DURATION]} min={MIN_DURATION} max={MAX_DURATION} step={1} onChange={handleOnDurationChange} renderLabel={renderDurationLabel}/>
<span className=" text-sm font-bold text-center mt-2.5">Durée</span>
<span className=" text-sm font-bold text-center mt-2.5">{t("playlist.duration")}</span>
</div>
<div className="w-full flex flex-col justify-center p-2">
<BsmRange values={[minNps ?? MIN_NPS, maxNps ?? MAX_NPS]} min={MIN_NPS} max={MAX_NPS} step={0.1} onChange={handleOnNpsChange} renderLabel={v => renderSimpleMinMaxLabel(v, MAX_NPS)}/>
<span className=" text-sm font-bold text-center mt-2.5">Notes par secondes</span>
<span className=" text-sm font-bold text-center mt-2.5">{t("playlist.nps")}</span>
</div>
</motion.div>
)
@@ -57,8 +57,6 @@ export type LocalPlaylistsListRef = {
exportPlaylists: () => Promise<void>;
}
// TODO : Translate
export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(({ version, className, filter: playlistFiler, search, isActive, linkedState }, forwardedRef) => {
const t = useTranslation();
@@ -90,7 +88,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
useImperativeHandle(forwardedRef, () => ({
createPlaylist: async () => {
const modalRes = await modals.openModal(EditPlaylistModal, { noStyle: true, data: { version, maps$ } });
const modalRes = await modals.openModal(EditPlaylistModal, { noStyle: true, data: { maps$ } });
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; }
@@ -98,14 +96,14 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
if(error){
logRenderError("Error occured while creating playlist", error);
notification.notifyError({ title: "Erreur lors de la création de la playlist", desc: "Une erreur est survenue lors de la création de la playlist." });
notification.notifyError({ title: "playlist.error-playlist-creation-title", desc: "playlist.error-playlist-creation-desc" });
return;
}
setPlaylists([result, ...playlists$.value]);
const notifRes = await notification.notifySuccess({ title: "Playlist créée !", desc: "La playlist a été créée avec succès. Tu peut maintenant synchroniser ses maps !", duration: 8000, actions: [
{ id: "sync", title: "Synchroniser les maps" }
const notifRes = await notification.notifySuccess({ title: "playlist.playlist-created-title", desc: "playlist.playlist-created-desc", duration: 8000, actions: [
{ id: "sync", title: "playlist.synchronize-maps" }
]});
if(notifRes === "sync"){
@@ -127,12 +125,12 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
if(error){
logRenderError("Error occured while synchronizing playlists", error);
notification.notifyError({ title: "Erreur lors de la synchronisation des playlists", desc: "Une erreur est survenue lors de la synchronisation des playlists." });
notification.notifyError({ title: "playlist.error-playlists-synchronization-title", desc: "playlist.error-playlists-synchronization-desc" });
return;
}
if(result.every(res => res.current === res.total)){
notification.notifySuccess({ title: "Playlists synchronisées !", desc: "Les playlists et leurs maps ont été téléchargées.", duration: 5000 });
notification.notifySuccess({ title: "playlist.playlists-synchronized-title", desc: "playlist.playlists-synchronized-desc", duration: 5000 });
}
},
exportPlaylists: async () => {
@@ -159,11 +157,11 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
if(error){
logRenderError("Error occured while exporting playlists", error);
notification.notifyError({ title: "Erreur lors de l'exportation des playlists", desc: "Une erreur est survenue lors de l'exportation des playlists." });
notification.notifyError({ title: "playlist.playlists-export-error-title", desc: "playlist.playlists-export-error-desc" });
return;
}
notification.notifySuccess({ title: "Playlists exportées !", desc: "Les playlists et leurs maps ont été exportées.", duration: 5000 });
notification.notifySuccess({ title: "playlist.playlists-exported-title", desc: exportMaps ? "playlist.playlists-exported-desc" : "playlist.playlists-with-maps-exported-desc", duration: 5000 });
progess.hide(true);
},
@@ -229,7 +227,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
return lastValueFrom(obs$).then(res => {
if(res.current === res.total){
notification.notifySuccess({ title: "Playlist synchronisée !", desc: "La playlist et ses maps on été téléchargées.", duration: 5000 })
notification.notifySuccess({ title: "playlist.playlists-synchronized-title", desc: "playlist.playlists-synchronized-desc", duration: 5000 })
}
});
}
@@ -255,7 +253,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
if(error){
logRenderError("Error occured while deleting playlist", error);
notification.notifyError({ title: "Erreur lors de la suppression de la playlist", desc: "Une erreur est survenue lors de la suppression de la playlist." });
notification.notifyError({ title: "playlist.playlist-delete-error-title", desc: "playlist.playlist-delete-error-desc" });
progess.hide(true);
return;
}
@@ -268,6 +266,8 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
}
}
notification.notifySuccess({ title: "playlist.playlists-deleted-title", desc: "playlist.playlists-deleted-desc", duration: 5000 });
progess.hide(true);
};
@@ -314,7 +314,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
}
const tmpPlaylist: LocalBPList = { ...playlist, playlistTitle: needClone ? `${playlist.playlistTitle} (${t("Clone")})` : playlist.playlistTitle };
const modalRes = await modals.openModal(EditPlaylistModal, { noStyle: true, data: { version, maps$, playlist: tmpPlaylist } });
const modalRes = await modals.openModal(EditPlaylistModal, { noStyle: true, data: { maps$, playlist: tmpPlaylist } });
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; }
@@ -322,7 +322,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
if(error){
logRenderError("Error occured while editing playlist", error);
notification.notifyError({ title: "Erreur lors de la modification de la playlist", desc: "Une erreur est survenue lors de la modification de la playlist." });
notification.notifyError({ title: "playlist.playlist-edit-error-title", desc: "playlist.playlist-edit-error-desc" });
return;
}
@@ -338,8 +338,8 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
setPlaylists(newPlaylists);
const notifRes = await notification.notifySuccess({ title: "Playlist modifiée !", desc: "La playlist a été modifiée avec succès. Tu peut maintenant synchroniser ses maps !", duration: 8000, actions: [
{ id: "sync", title: "Synchroniser les maps" }
const notifRes = await notification.notifySuccess({ title: "playlist.playlist-edited-title", desc: "playlist.playlist-edited-desc", duration: 8000, actions: [
{ id: "sync", title: "playlist.synchronize-maps" }
]});
if(notifRes === "sync"){
@@ -409,7 +409,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
{(() => {
if(playlistsLoading){
return (
<BsContentLoader className="w-full h-full flex justify-center flex-col items-center" value$={loadPercent$} text="Chargement des playlists"/>
<BsContentLoader className="w-full h-full flex justify-center flex-col items-center" value$={loadPercent$} text="playlist.playlists-loading"/>
)
}
@@ -432,10 +432,10 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
return (
<div className="h-full flex flex-col items-center justify-center flex-wrap gap-1 text-gray-800 dark:text-gray-200">
<BsmImage className="h-32" image={BeatConflict} />
<span className="font-bold">Aucune playlist</span>
<span className="font-bold">{t("playlist.no-playlists")}</span>
<BsmButton
className="font-bold rounded-md p-2"
text="Télécharger des playlists"
text="playlist.download-playlists"
typeColor="primary"
withBar={false}
onClick={e => {
@@ -16,6 +16,7 @@ import { useObservable } from 'renderer/hooks/use-observable.hook';
import { BsmBasicSpinner } from 'renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component';
import defaultImage from "../../../../../assets/images/default-version-img.jpg";
import equal from 'fast-deep-equal';
import { useTranslation } from 'renderer/hooks/use-translation.hook';
export type PlaylistItemComponentProps = {
title?: string;
@@ -62,6 +63,7 @@ export const PlaylistItem = memo(({ title,
onClickEdit
}: PlaylistItemComponentProps) => {
const t = useTranslation();
const color = useThemeColor("first-color");
const [hovered, setHovered] = useState(false);
@@ -82,8 +84,6 @@ export const PlaylistItem = memo(({ title,
return duration > 3600 ? dateFormat(duration * 1000, "h:MM:ss") : dateFormat(duration * 1000, "MM:ss");
})();
// TODO : Translate
return (
<motion.li className='relative flex-grow basis-0 min-w-80 h-28 cursor-pointer group' onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)}>
<GlowEffect visible={selected || hovered}/>
@@ -101,7 +101,7 @@ export const PlaylistItem = memo(({ title,
</div>
<div className="h-full py-2.5 text-white">
<h1 className="font-bold text-xl tracking-wide line-clamp-1 w-fit">{title}</h1>
<p className="text-xs font-bold">Créé par <span className="brightness-200" style={{color}}>{author}</span></p>
<p className="text-xs font-bold">{t("playlist.created-by")} <span className="brightness-200" style={{color}}>{author}</span></p>
<div className="flex flex-row flex-wrap w-full gap-2 mt-1">
{ nbMapsText && <div className="flex items-center text-sm h-5 gap-0.5"> <MapIcon className='h-full aspect-square'/> <span className="mb-0.5">{nbMapsText}</span> </div> }
@@ -117,7 +117,7 @@ export const PlaylistItem = memo(({ title,
<motion.div className="flex flex-col justify-center items-center flex-wrap gap-0.5 size-full px-1 *:size-6 *:!bg-inherit *:p-0.5 *:rounded-md" animate={{opacity: hovered || isDownloading ? 1 : 0}} transition={{duration: 0}}>
{(isDownloading || isInQueue) && onClickCancelDownload ? (
<Tippy content={isDownloading ? "Arreter le téléchargement" : "Annuler le téléchargement"} placement="left" theme="default">
<Tippy content={isDownloading ? t("playlist.stop-download") : t("playlist.cancel-download")} placement="left" theme="default">
<BsmButton
icon="close"
className="hover:!bg-main-color-1 text-red-500 !p-0"
@@ -131,7 +131,7 @@ export const PlaylistItem = memo(({ title,
isDownloading ? (
<BsmBasicSpinner className="hover:!bg-main-color-1" spinnerClassName="brightness-75 dark:brightness-200" style={{ color }} thikness="3px"/>
) : !isInQueue ? (
<Tippy content="Synchronizer la playlist" placement="left" theme="default">
<Tippy content={t("playlist.synchronize-playlist")} placement="left" theme="default">
<BsmButton
icon="sync"
className="hover:!bg-main-color-1"
@@ -148,7 +148,7 @@ export const PlaylistItem = memo(({ title,
isDownloading ? (
<BsmBasicSpinner className="hover:!bg-main-color-1" spinnerClassName="brightness-75 dark:brightness-200" style={{ color }} thikness="3px"/>
) : !isInQueue ? (
<Tippy content="Télécharger la playlist" placement="left" theme="default">
<Tippy content={t("playlist.download-playlist")} placement="left" theme="default">
<BsmButton
icon="download"
className="hover:!bg-main-color-1"
@@ -162,7 +162,7 @@ export const PlaylistItem = memo(({ title,
) : (<></>)}
{onClickEdit ? (
<Tippy content="Editer la playlist" placement="left" theme="default">
<Tippy content={t("playlist.edit-playlist")} placement="left" theme="default">
<BsmButton
icon="edit"
className="hover:!bg-main-color-1"
@@ -173,7 +173,7 @@ export const PlaylistItem = memo(({ title,
/>
</Tippy>
) : (<></>)}
{onClickOpenFile ? <Tippy content="Afficher le fichier" placement="left" theme="default">
{onClickOpenFile ? <Tippy content={t("playlist.open-file")} placement="left" theme="default">
<BsmButton
icon="folder"
className="hover:!bg-main-color-1"
@@ -183,7 +183,7 @@ export const PlaylistItem = memo(({ title,
withBar={false}
/>
</Tippy> : (<></>)}
{(onClickDelete && !isDownloading && !isInQueue) ? <Tippy content="Supprimer" placement="left" theme="default">
{(onClickDelete && !isDownloading && !isInQueue) ? <Tippy content={t("misc.delete")} placement="left" theme="default">
<BsmButton
icon="trash"
className="hover:!bg-main-color-1 text-red-500"
@@ -5,28 +5,29 @@ import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import BeatRunning from "../../../../../assets/images/apngs/beat-running.png";
import Tippy from "@tippyjs/react";
export const LinkPlaylistModal: ModalComponent<boolean> = ({ resolver }) => {
const t = useTranslation();
const [keepMaps, setKeepMaps] = useState(true);
// TODO : Translate
return (
<form className="text-gray-800 dark:text-gray-200">
<h1 className="text-3xl uppercase tracking-wide w-full text-center">Lier les playlists</h1>
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("playlist.link-playlists")}</h1>
<BsmImage className="mx-auto h-24" image={BeatRunning} />
<p className="max-w-sm w-full">La liaison des playlists permet de partager les playlists entre toute les version. Une fois liée, cette version profitera des playlists partagées</p>
<p className="max-w-sm w-full text-sm italic mt-2">L'ajout et la suppression de playlists sera également partagé</p>
<p className="max-w-sm w-full">{t("playlist.link-playlist-desc")}</p>
<p className="max-w-sm w-full text-sm italic mt-2">{t("playlist.link-playlist-info")}</p>
<div className="relative h-5 flex my-3 items-center">
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1.5 z-[1]" checked={keepMaps} onChange={setKeepMaps} />
<span className="text-sm mb-0.5 cursor-help" title="Conserver les playlists déplacera les playlists de la version actuelle dans le dossier des playlists partagées. Dans le cas contraire elles seront perdues">
Conserver les playlists
</span>
<Tippy content={t("playlist.keep-playlists-tip")} placement="top" theme="default">
<span className="text-sm mb-0.5 cursor-help">
{t("playlist.keep-playlists")}
</span>
</Tippy>
</div>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4">
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.COMPLETED, data: keepMaps })} withBar={false} text="Lier les playlists" />
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.COMPLETED, data: keepMaps })} withBar={false} text="playlist.link-playlists" />
</div>
</form>
);
@@ -8,8 +8,6 @@ import BeatConflict from "../../../../../../assets/images/apngs/beat-conflict.pn
import { BPList } from "shared/models/playlists/playlist.interface";
import Tippy from "@tippyjs/react";
// TODO : Translate
export const DeletePlaylistModal: ModalComponent<boolean, BPList[]> = ({ resolver, options: { data }}) => {
const t = useTranslation();
@@ -20,21 +18,21 @@ export const DeletePlaylistModal: ModalComponent<boolean, BPList[]> = ({ resolve
return (
<form className="text-gray-800 dark:text-gray-200">
{!isMultiple ? (
<h1 className="text-3xl uppercase tracking-wide w-full text-center">Supprimer la playlist ?</h1>
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("playlist.delete-playlist-ask")}</h1>
) : (
<h1 className="text-3xl uppercase tracking-wide w-full text-center">Supprimer les playlists ?</h1>
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("playlist.delete-playlists-ask")}</h1>
)}
<BsmImage className="mx-auto h-24" image={BeatConflict} />
{!isMultiple ? (
<p className="max-w-sm w-full">{`Est-tu sûr de vouloir supprimer la playlist "${data.at(0)?.playlistTitle}" ?`}</p>
<p className="max-w-sm w-full">{t("playlist.delete-playlist-desc", { playlistTitle: data.at(0)?.playlistTitle })}</p>
) : (
<p className="max-w-sm w-full">{`Est-tu sûr de vouloir supprimer les ${data.length} playlists ?`}</p>
<p className="max-w-sm w-full">{t("playlist.delete-playlists-desc", { nb: `${data?.length ?? 0}` })}</p>
)}
<div className="flex items-center relative py-2 gap-1">
<BsmCheckbox className="h-5 relative z-[1]" checked={deleteMaps} onChange={val => setDeleteMaps(() => val)} />
<Tippy placement="top" content={isMultiple ? "Si activé, toutes les maps de des playlists seront supprimées" : "Si activé, toutes les maps de la playlist seront supprimées"} theme="default">
<span className="italic cursor-help">Supprimer les maps</span>
<Tippy placement="top" content={isMultiple ? t("playlist.delete-playlists-maps-tip") : t("playlist.delete-playlist-maps-tip")} theme="default">
<span className="italic cursor-help">{t("playlist.delete-maps")}</span>
</Tippy>
</div>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-2">
@@ -26,8 +26,6 @@ type Props = {
const MAX_NPS = 17;
const MIN_NPS = 0;
// TODO : Translate
export function DownloadPlaylistFilterPanel({ className, params, onChange, onSubmit }: Props) {
const t = useTranslation();
@@ -116,15 +114,15 @@ export function DownloadPlaylistFilterPanel({ className, params, onChange, onSub
<h2 className="mb-0.5 uppercase text-sm">{t("maps.map-filter-panel.specificities")}</h2>
<div className="flex flex-row h-6 gap-1">
<BsmCheckbox className="relative h-full aspect-square" checked={curated} onChange={setCurated}/>
<span>Curated</span>
<span>{t("playlist.curated")}</span>
</div>
<div className="flex flex-row h-6 gap-1">
<BsmCheckbox className="relative h-full aspect-square" checked={verified} onChange={setVerified}/>
<span>Verified mapper</span>
<span>{t("playlist.verified-mapper")}</span>
</div>
<div className="flex flex-row h-6 gap-1">
<BsmCheckbox className="relative h-full aspect-square" checked={includeEmpty} onChange={setIncludeEmpty}/>
<span>Include empty playlists</span>
<span>{t("playlist.empty-playlists")}</span>
</div>
</div>
<div className="flex flex-col items-end grow shrink-0 min-w-64 gap-2">
@@ -133,7 +131,7 @@ export function DownloadPlaylistFilterPanel({ className, params, onChange, onSub
<span className=" text-sm font-bold text-center mt-2.5">{t("maps.map-filter-panel.nps")}</span>
</div>
<div className="w-full">
<DateRangePicker label="Start date — End date" value={dateRange} onChange={setDateRange} classNames={{
<DateRangePicker label={t("playlist.date-picker.start-date-end-date")} value={dateRange} onChange={setDateRange} classNames={{
inputWrapper: "pointer-events-none bg-light-main-color-1 hover:bg-light-main-color-1 dark:bg-main-color-1 dark:hover:bg-main-color-1 hover:brightness-75",
input: "pointer-events-auto",
selectorButton: "pointer-events-auto",
@@ -142,11 +140,11 @@ export function DownloadPlaylistFilterPanel({ className, params, onChange, onSub
base: "w-[var(--calendar-width)] pb-2",
wrapper: "flex flex-row flex-wrap -my-2.5 py-2.5 px-3 gap-1 overflow-scroll max-w-[var(--calendar-width)] w-[var(--calendar-width)]"
}}>
<CustomRadio value="days_0">All</CustomRadio>
<CustomRadio value="days_1">Last 24h</CustomRadio>
<CustomRadio value="weeks_1">Last week</CustomRadio>
<CustomRadio value="months_1">Last month</CustomRadio>
<CustomRadio value="months_3">Last 3 months</CustomRadio>
<CustomRadio value="days_0">{t("playlist.date-picker.all")}</CustomRadio>
<CustomRadio value="days_1">{t("playlist.date-picker.last-24h")}</CustomRadio>
<CustomRadio value="weeks_1">{t("playlist.date-picker.last-week")}</CustomRadio>
<CustomRadio value="months_1">{t("playlist.date-picker.last-month")}</CustomRadio>
<CustomRadio value="months_3">{t("playlist.date-picker.3-last-month")}</CustomRadio>
</RadioGroup>
}/>
</div>
@@ -6,6 +6,7 @@ import { cn } from "renderer/helpers/css-class.helpers";
import { useConstant } from "renderer/hooks/use-constant.hook";
import { PlaylistSearchParams, BsvSearchOrder } from "shared/models/maps/beat-saver.model";
import { DownloadPlaylistFilterPanel, FilterPlaylistSearchParams } from "./download-playlist-filter-panel.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
type Props = {
className?: string;
@@ -13,14 +14,13 @@ type Props = {
onSubmit?: (value: Omit<PlaylistSearchParams, "page">) => void;
}
// TODO : Translate
export function DownloadPlaylistModalHeader({ className, value, onSubmit }: Props) {
const t = useTranslation();
const dropDownFilterRef = useRef(null);
const [query, setQuery] = useState<string>(value?.q || "");
const [order, setOrder] = useState<BsvSearchOrder>(value?.sortOrder || BsvSearchOrder.Latest);
const [order, setOrder] = useState<BsvSearchOrder>(value?.sortOrder || BsvSearchOrder.Relevance);
const [filter, setFilter] = useState<FilterPlaylistSearchParams>({});
const searchParams: Omit<PlaylistSearchParams, "page"> = { q: query, sortOrder: order };
@@ -54,7 +54,7 @@ export function DownloadPlaylistModalHeader({ className, value, onSubmit }: Prop
<BsmDropdownButton ref={dropDownFilterRef} buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1 !bg-light-main-color-1 dark:!bg-main-color-1" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" withBar={false}>
<DownloadPlaylistFilterPanel className="z-10 translate-y-1" params={filter} onSubmit={handleFilterSubmit}/>
</BsmDropdownButton>
<input className="h-full bg-theme-1 rounded-full px-2 grow pb-0.5" type="text" placeholder="Rechercher une playlist" value={query} onChange={e => setQuery(e.target.value)} />
<input className="h-full bg-theme-1 rounded-full px-2 grow pb-0.5" type="text" placeholder={t("playlist.search-playlist")} value={query} onChange={e => setQuery(e.target.value)} />
<BsmButton className="shrink-0 rounded-full py-1 px-3 !bg-theme-1 flex justify-center items-center capitalize" icon="search" text="modals.download-maps.search-btn" withBar={false} onClick={() => submit(searchParams)} />
<BsmSelect className="bg-theme-1 rounded-full px-1 pb-0.5 text-center cursor-pointer" options={sortOptions} selected={order} onChange={handleOrderChange}/>
</form>
@@ -19,13 +19,14 @@ import BeatWaiting from "../../../../../../../assets/images/apngs/beat-waiting.p
import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict.png"
import { cn } from "renderer/helpers/css-class.helpers"
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component"
// TODO : Translate
import { useTranslation } from "renderer/hooks/use-translation.hook"
export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ownedPlaylists$: Observable<LocalBPListsDetails[]>, ownedMaps$: Observable<BsmLocalMap[]>}> = (
{ resolver, options: { data: { version, ownedPlaylists$, ownedMaps$ }} }
{ options: { data: { version, ownedPlaylists$, ownedMaps$ }} }
) => {
const t = useTranslation();
const modal = useService(ModalService);
const beatSaver = useService(BeatSaverService);
const playlistDownloader = useService(PlaylistDownloaderService);
@@ -109,7 +110,7 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
<div className="w-full flex flex-col justify-center items-center mt-44">
<BsmImage className={cn(["size-32", !error && "spin-loading"])} image={error ? BeatConflict : BeatWaiting} />
<p className="text-lg font-bold px-10 text-center">
{error ? "Une erreur est survenue lors du chargement des playlists" : "Chargement des playlists..."}
{error ? t("playlist.error-occur-while-loading-playlists") : t("playlist.playlists-loading")}
</p>
</div>
)
@@ -119,7 +120,7 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
<div className="w-full flex flex-col justify-center items-center mt-44">
<BsmImage className="size-32" image={BeatConflict} />
<p className="text-lg font-bold px-10 text-center">
Aucune playlist trouvée
{t("playlist.no-playlists-found")}
</p>
</div>
);
@@ -7,6 +7,7 @@ import { SteamDownloaderService } from "renderer/services/bs-version-download/st
import { IpcService } from "renderer/services/ipc.service";
import { lastValueFrom } from "rxjs";
import { logRenderError } from "renderer";
import { useTranslation } from "renderer/hooks/use-translation.hook";
type OutProps = {
playlistTitle: string;
@@ -25,6 +26,8 @@ export const EditPlaylistInfosModal: ModalComponent<OutProps, Props> = ({ resolv
isEdit
}}}) => {
const t = useTranslation();
const steamDownloader = useService(SteamDownloaderService);
const ipc = useService(IpcService);
@@ -51,34 +54,34 @@ export const EditPlaylistInfosModal: ModalComponent<OutProps, Props> = ({ resolv
};
return (
<div className="text-gray-800 dark:text-gray-200">
<div className="text-gray-800 dark:text-gray-200 min-w-80">
<h1 className="text-3xl uppercase tracking-wide w-full text-center mb-4 px-4">
{isEdit ? "Modifier la playlist" : "Créer une playlist"}
{isEdit ? t("playlist.edit-playlist") : t("playlist.create-a-playlist")}
</h1>
<div className="w-full flex flex-col justify-center items-center">
<button className="flex justify-center items-center relative size-36 border-2 border-gray-400 bg-theme-1 rounded-md overflow-hidden" onClick={handleClickImage}>
{base64 ? (
<BsmImage className="absolute size-full cursor-pointer" base64={base64} />
) : (
<span className="absolute size-full flex justify-center items-center p-2">Choisir une image</span>
<span className="absolute size-full flex justify-center items-center p-2">{t("playlist.choose-image")}</span>
)}
</button>
<div className="w-full">
<label className="font-bold cursor-pointer tracking-wide" htmlFor="playlist-title">Titre</label>
<input id="playlist-title" type="text" className="w-full bg-theme-1 px-1 py-0.5 rounded-md outline-none h-9" value={title} placeholder="Titre de la playlist" onChange={e => setTitle(e.target.value)}/>
<label className="font-bold cursor-pointer tracking-wide" htmlFor="playlist-title">{t("playlist.title")}</label>
<input id="playlist-title" type="text" className="w-full bg-theme-1 px-1 py-0.5 rounded-md outline-none h-9" value={title} placeholder={t("playlist.playlist-title")} onChange={e => setTitle(e.target.value)}/>
</div>
<div className="w-full mt-1.5">
<label className="font-bold cursor-pointer tracking-wide" htmlFor="playlist-desc">Description</label>
<textarea id="playlist-desc" className="w-full bg-theme-1 px-1 py-0.5 rounded-md outline-none max-h-40 min-h-8" value={description} placeholder="Description de la playlist" onChange={e => setDescription(e.target.value)} />
<label className="font-bold cursor-pointer tracking-wide" htmlFor="playlist-desc">{t("playlist.description")}</label>
<textarea id="playlist-desc" className="w-full bg-theme-1 px-1 py-0.5 rounded-md outline-none max-h-40 min-h-8" value={description} placeholder={t("playlist.playlist-description")} onChange={e => setDescription(e.target.value)} />
</div>
<div className="w-full">
<label className="font-bold cursor-pointer tracking-wide" htmlFor="playlist-author">Auteur</label>
<input id="playlist-author" type="text" className="w-full bg-theme-1 px-1 py-0.5 rounded-md outline-none h-9" value={author} placeholder="Auteur de la playlist" onChange={e => setAuthor(e.target.value)}/>
<label className="font-bold cursor-pointer tracking-wide" htmlFor="playlist-author">{t("playlist.author")}</label>
<input id="playlist-author" type="text" className="w-full bg-theme-1 px-1 py-0.5 rounded-md outline-none h-9" value={author} placeholder={t("playlist.playlist-author")} onChange={e => setAuthor(e.target.value)}/>
</div>
</div>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4 h-8">
<BsmButton typeColor="cancel" className="rounded-md flex justify-center items-center transition-all h-full" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
<BsmButton typeColor="primary" className="rounded-md flex justify-center items-center transition-all h-full" onClick={submit} withBar={false} text="Enregistrer" />
<BsmButton typeColor="primary" className="rounded-md flex justify-center items-center transition-all h-full" onClick={submit} withBar={false} text="playlist.save" />
</div>
</div>
)
@@ -39,14 +39,13 @@ import BeatWaiting from "../../../../../../../assets/images/apngs/beat-waiting.p
import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict.png";
type Props = {
version?: BSVersion;
maps$: Observable<BsmLocalMap[]>;
playlist?: LocalBPList;
}
type PlaylistMap = { map: (BsmLocalMap|SongDetails|BsvMapDetail), difficulties?: BPListDifficulty[] };
export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, options: { data: { version, maps$, playlist } } }) => {
export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, options: { data: { maps$, playlist } } }) => {
const t = useTranslation();
const color = useThemeColor("first-color");
@@ -460,7 +459,7 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
</button>
{(() => {
if(!playlistMaps || !localMaps){
return <div className="flex items-center justify-center w-full h-full">Loading...</div>
return <div className="flex items-center justify-center w-full h-full">{t("playlist.loading")}</div>
}
else{
return (
@@ -468,7 +467,7 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
<div className="grow flex flex-row min-h-0 gap-2.5">
<div className="flex flex-col grow basis-0 min-w-0">
<form className="h-8 flex flex-row gap-2 w-full mb-1.5 min-w-0" onSubmit={e => {e.preventDefault(); handleNewSearch()}}>
<BsmSelect className="bg-theme-1 h-full rounded-full text-center pb-0.5" options={[{ text: "Installée(s)", value: 0 }, { text: "BeatSaver", value: 1 }]} onChange={setAvailableMapsSource}/>
<BsmSelect className="bg-theme-1 h-full rounded-full text-center pb-0.5" options={[{ text: t("playlist.installed"), value: 0 }, { text: "BeatSaver", value: 1 }]} onChange={setAvailableMapsSource}/>
<input className="h-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2 grow pb-0.5 min-w-0" type="text" placeholder={t("pages.version-viewer.maps.search-bar.search-placeholder")} value={availableMapsSearch} onChange={e => setAvailableMapsSearch(() => e.target.value)} />
{availableMapsSource === 1 && (
<BsmButton className="h-full aspect-square z-[1] flex justify-center p-1 rounded-full min-w-0 shrink-0 !bg-light-main-color-1 dark:!bg-main-color-1" icon="search" onClick={handleNewSearch} withBar={false}/>
@@ -488,7 +487,7 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
return (
<div className="flex flex-col items-center justify-center size-full">
<BsmImage className="size-24 spin-loading" image={BeatWaiting}/>
<span className="text-sm italic leading-4">Loading...</span>
<span className="text-sm italic leading-4">{t("playlist.loading")}</span>
</div>
);
}
@@ -497,7 +496,7 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
return (
<div className="flex flex-col items-center justify-center size-full">
<BsmImage className="size-20" image={BeatConflict}/>
<span className="text-sm italic leading-4 w-3/4 text-center">Aucune map trouvée</span>
<span className="text-sm italic leading-4 w-3/4 text-center">{t("playlist.no-map-found")}</span>
</div>
);
}
@@ -514,16 +513,16 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
</div>
<div className="w-full h-4 flex justify-start">
<span className="text-xs italic leading-4">Hold shift or ctrl to select multiples maps</span>
<span className="text-xs italic leading-4">{t("playlist.edit-playlist-shortcuts")}</span>
</div>
</div>
<div className="shrink-0 flex flex-col gap-2.5 pb-4 pt-10">
<Tippy content="Ajouter à la playlist" theme="default" placement="left">
<Tippy content={t("playlist.add-to-playlist")} theme="default" placement="left">
<button className="grow w-9 rounded-md cursor-pointer transition-transform duration-150 hover:brightness-110 active:scale-95" style={{backgroundColor: color, color: getCorrectTextColor(color)}} type="button" onClick={addMapsToPlaylist}>
<ChevronTopIcon className="origin-center rotate-90"/>
</button>
</Tippy>
<Tippy content="Enlever de la playlist" theme="default" placement="right">
<Tippy content={t("playlist.remove-from-playlist")} theme="default" placement="right">
<button className="grow w-9 rounded-md cursor-pointer transition-transform duration-150 hover:brightness-110 active:scale-95" style={{backgroundColor: color, color: getCorrectTextColor(color)}} type="button" onClick={removeMapsFromPlaylist}>
<ChevronTopIcon className="origin-center -rotate-90"/>
</button>
@@ -542,7 +541,7 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
return(
<div className="flex flex-col items-center justify-center size-full">
<BsmImage className="size-24 spin-loading" image={BeatWaiting}/>
<span className="text-sm italic leading-4">Loading...</span>
<span className="text-sm italic leading-4">{t("playlist.loading")}</span>
</div>
);
}
@@ -550,7 +549,7 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
return (
<div className="flex flex-col items-center justify-center size-full">
<BsmImage className="size-20" image={BeatConflict}/>
<span className="text-sm italic leading-4 w-3/4 text-center">La playlist est vide</span>
<span className="text-sm italic leading-4 w-3/4 text-center">{t("playlist.playlist-is-empty")}</span>
</div>
);
}
@@ -592,8 +591,8 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
</div>
</div>
<footer className="flex justify-center items-center gap-2 h-8 mt-2.5">
<BsmButton className="rounded-md text-center h-full grow basis-0 flex justify-center items-center" typeColor="cancel" text="Annuler" withBar={false} onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })}/>
<BsmButton className="rounded-md text-center h-full grow basis-0 flex justify-center items-center" typeColor="primary" text="Continuer" withBar={false} onClick={handleContinue}/>
<BsmButton className="rounded-md text-center h-full grow basis-0 flex justify-center items-center" typeColor="cancel" text="misc.cancel" withBar={false} onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })}/>
<BsmButton className="rounded-md text-center h-full grow basis-0 flex justify-center items-center" typeColor="primary" text="playlist.continue" withBar={false} onClick={handleContinue}/>
</footer>
</div>
)
@@ -8,8 +8,6 @@ import BeatConflict from "../../../../../../assets/images/apngs/beat-conflict.pn
import { BPList } from "shared/models/playlists/playlist.interface";
import Tippy from "@tippyjs/react";
// TODO : Translate
export const ExportPlaylistModal: ModalComponent<boolean, BPList[]> = ({ resolver, options: { data }}) => {
const t = useTranslation();
@@ -20,30 +18,30 @@ export const ExportPlaylistModal: ModalComponent<boolean, BPList[]> = ({ resolve
return (
<form className="max-w-sm text-gray-800 dark:text-gray-200">
{!isMultiple ? (
<h1 className="text-3xl uppercase tracking-wide w-full text-center">Exporter la playlist ?</h1>
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("playlist.export-playlist-ask")}</h1>
) : (
<h1 className="text-3xl uppercase tracking-wide w-full text-center">Exporter les playlists ?</h1>
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("playlist.export-playlists-ask")}</h1>
)}
<BsmImage className="mx-auto h-24" image={BeatConflict} />
{!isMultiple ? (
<p className="w-full">{`Est-tu sûr de vouloir exporter la playlist "${data.at(0)?.playlistTitle}" ?`}</p>
<p className="w-full">{t("playlist.export-playlist-desc", { playlistTitle: data.at(0)?.playlistTitle })}</p>
) : (
<p className="w-full">{`Est-tu sûr de vouloir exporter les ${data.length} playlists ?`}</p>
<p className="w-full">{t("playlist.export-playlists-desc", { nb: `${data.length ?? 0}` })}</p>
)}
<div className="flex items-center relative py-2 gap-1">
<BsmCheckbox className="h-5 relative z-[1]" checked={exportMaps} onChange={val => setExportMaps(() => val)} />
<Tippy
placement="top"
content={isMultiple ? "Si activé, toutes les maps des playlists seront également exportées" : "Si activé, toutes les maps de la playlist seront également exportées"}
content={isMultiple ? t("playlist.export-playlists-maps-tip") : t("playlist.export-playlist-maps-tip")}
theme="default"
>
<span className="italic cursor-help">Exporter les maps</span>
<span className="italic cursor-help">{t("playlist.export-maps")}</span>
</Tippy>
</div>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-2">
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.COMPLETED, data: exportMaps })} withBar={false} text="Exporter" />
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.COMPLETED, data: exportMaps })} withBar={false} text="playlist.export" />
</div>
</form>
);
@@ -2,21 +2,22 @@ import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"
import BeatConflict from "../../../../../../assets/images/apngs/beat-conflict.png";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
// TODO : Translate
import { useTranslation } from "renderer/hooks/use-translation.hook";
export const NeedCloneEditPlaylistModal: ModalComponent<void, void> = ({ resolver }) => {
const t = useTranslation();
return (
<form className="text-gray-800 dark:text-gray-200 flex flex-col gap-2 max-w-sm">
<h1 className="text-3xl uppercase tracking-wide w-full text-center">Attention</h1>
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("playlist.need-clone-title")}</h1>
<BsmImage className="mx-auto h-24" image={BeatConflict} />
<p className="w-full">Cette playlist a é téléchargée depuis un site externe et contient un lien de synchronisation.</p>
<p className="w-full">Pour éviter de perdre vos modifications lors d'une synchronisation, la playlist va être dupliquée et son lien de synchronisation supprimé.</p>
<p className="w-full">Vous pourrez ensuite, si vous le souhaitez, supprimer la playlist originale.</p>
<p className="w-full">{t("playlist.need-clone-desc-1")}</p>
<p className="w-full">{t("playlist.need-clone-desc-2")}</p>
<p className="w-full">{t("playlist.need-clone-desc-3")}</p>
<div className="grid grid-flow-col grid-cols-2 gap-2 mt-2 h-8">
<BsmButton typeColor="cancel" className="rounded-md flex justify-center items-center transition-all h-full" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
<BsmButton typeColor="primary" className="rounded-md flex justify-center items-center transition-all h-full" onClick={() => resolver({ exitCode: ModalExitCode.COMPLETED })} withBar={false} text="J'ai compris" />
<BsmButton typeColor="primary" className="rounded-md flex justify-center items-center transition-all h-full" onClick={() => resolver({ exitCode: ModalExitCode.COMPLETED })} withBar={false} text="playlist.understood" />
</div>
</form>
);
@@ -19,6 +19,7 @@ import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { cn } from "renderer/helpers/css-class.helpers";
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component";
import { MapItemComponentPropsMapper } from "shared/mappers/map/map-item-component-props.mapper";
import { useTranslation } from "renderer/hooks/use-translation.hook";
type Props = {
version: BSVersion;
@@ -26,10 +27,10 @@ type Props = {
installedMaps$: Observable<BsmLocalMap[]>;
}
// TODO : Translate
export const BsvPlaylistDetailsModal: ModalComponent<void, Props> = ({ resolver, options: { data: { playlist, installedMaps$, version } } }) => {
const t = useTranslation();
const beatsaver = useService(BeatSaverService);
const mapsDownloader = useService(MapsDownloaderService);
@@ -111,7 +112,7 @@ export const BsvPlaylistDetailsModal: ModalComponent<void, Props> = ({ resolver,
<div className="w-full flex flex-col justify-center items-center mt-10">
<BsmImage className={cn(["size-32", !error && "spin-loading"])} image={error ? BeatConflict : BeatWaiting} />
<p className="text-lg font-bold px-10 text-center">
{error ? "Une erreur est survenue lors du chargement de la playlist" : "Chargement des maps..."}
{error ? t("playlist.error-occur-while-loading-playlist") : t("playlist.loading-maps")}
</p>
</div>
);
@@ -122,7 +123,7 @@ export const BsvPlaylistDetailsModal: ModalComponent<void, Props> = ({ resolver,
<div className="w-full flex flex-col justify-center items-center mt-10">
<BsmImage className="size-32" image={BeatConflict} />
<p className="text-lg font-bold px-10 text-center">
Aucune map trouvée pour cette playlist
{t("playlist.no-maps-found-for-playlist")}
</p>
</div>
);
@@ -16,8 +16,7 @@ import { useConstant } from "renderer/hooks/use-constant.hook";
import { useCallback } from "react";
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component";
import { MapItemComponentPropsMapper } from "shared/mappers/map/map-item-component-props.mapper";
// TODO : Translate
import { useTranslation } from "renderer/hooks/use-translation.hook";
interface Props {
version: BSVersion;
@@ -27,6 +26,8 @@ interface Props {
export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver, options}) => {
const t = useTranslation();
const playlistDownloader = useService(PlaylistDownloaderService);
const localPlaylist = useObservable(() => options.data.localPlaylist$, null);
@@ -90,7 +91,7 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
<div className="grow flex justify-center items-center flex-col">
<BsmImage image={BeatConflict} className="size-28"/>
<div className="text-white font-bold w-fit space-y-1.5 flex flex-col justify-center items-center -translate-y-5">
<p>La Playlist ne contient aucune map</p>
<p>{t("playlist.playlist-contain-no-maps")}</p>
</div>
</div>
);
@@ -101,8 +102,8 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
<div className="grow flex justify-center items-center flex-col">
<BsmImage image={BeatConflict} className="size-28"/>
<div className="text-white font-bold w-fit space-y-1.5 flex flex-col justify-center items-center -translate-y-5">
<p>Aucune maps installée pour cette playlist</p>
<BsmButton withBar={false} onClick={installPlaylist} className="rounded-md h-8 flex items-center justify-center px-4" typeColor="primary" text="Télécharger.les.maps"/>
<p>{t("playlist.no-map-installed-for-playlist")}</p>
<BsmButton withBar={false} onClick={installPlaylist} className="rounded-md h-8 flex items-center justify-center px-4" typeColor="primary" text="playlist.download-maps"/>
</div>
</div>
);
@@ -113,7 +114,7 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
<div className="grow flex justify-center items-center flex-col">
<BsmImage image={BeatConflict} className="size-28"/>
<div className="text-white font-bold w-fit space-y-1.5 flex flex-col justify-center items-center -translate-y-5">
<p>La Playlist est en attente de téléchargment</p>
<p>{t("playlist.playlist-is-waiting-to-download")}</p>
</div>
</div>
);
@@ -8,6 +8,7 @@ import { NpsIcon } from "renderer/components/svgs/icons/nps-icon.component";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { ThemeColorGradientSpliter } from "renderer/components/shared/theme-color-gradient-spliter.component";
import { CrossIcon } from "renderer/components/svgs/icons/cross-icon.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
export type PlaylistDetailsTemplateProps = {
title: string;
@@ -26,6 +27,7 @@ export type PlaylistDetailsTemplateProps = {
export function PlaylistDetailsTemplate({title, imagebase64, imageUrl, author, description, nbMaps, duration, nbMappers, minNps, maxNps, children, onClose}: PlaylistDetailsTemplateProps) {
const t = useTranslation();
const color = useThemeColor("first-color");
const nbMapsText = nbMaps ? Intl.NumberFormat(undefined, { notation: "compact" }).format(nbMaps).trim() : null;
@@ -54,7 +56,7 @@ export function PlaylistDetailsTemplate({title, imagebase64, imageUrl, author, d
<div className="h-full px-3 text-white z-[1]">
<h1 className="font-bold text-xl tracking-wide line-clamp-1 w-fit">{title}</h1>
<p className="line-clamp-2" title={description}>{description}</p>
<p className="text-xs font-bold">Créé par <span className="brightness-200" style={{color}}>{author}</span></p>
<p className="text-xs font-bold">{t("playlist.created-by")} <span className="brightness-200" style={{color}}>{author}</span></p>
<div className="flex flex-row flex-wrap w-full gap-2 mt-1">
{ nbMapsText && <div className="flex items-center text-sm h-5 gap-0.5"> <MapIcon className='h-full aspect-square'/> <span className="mb-0.5">{nbMapsText}</span> </div> }
@@ -4,6 +4,7 @@ import { BsmImage } from "renderer/components/shared/bsm-image.component";
import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict.png";
import { Observable } from "rxjs";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { useTranslation } from "renderer/hooks/use-translation.hook";
type Props = {
isPlaylistInQueue$: Observable<boolean>;
@@ -14,6 +15,8 @@ type Props = {
export function PlaylistHeaderState({isPlaylistInQueue$, isPlaylistDownloading$, isMissingMaps$, installPlaylist}: Props) {
const t = useTranslation();
const isPlaylistInQueue = useObservable(() => isPlaylistInQueue$, false);
const isPlaylistDownloading = useObservable(() => isPlaylistDownloading$, false);
const isMissingMaps = useObservable(() => isMissingMaps$, false);
@@ -25,10 +28,10 @@ export function PlaylistHeaderState({isPlaylistInQueue$, isPlaylistDownloading$,
<>
<BsmImage image={BeatConflict} className="size-24"/>
<div className="text-white font-bold w-fit space-y-1.5 flex flex-col justify-center items-center">
<p>La playlist est en cours de téléchargement</p>
<p>{t("playlist.playlist-is-downloading")}</p>
</div>
</>
)
);
}
if(isPlaylistInQueue) {
@@ -36,18 +39,18 @@ export function PlaylistHeaderState({isPlaylistInQueue$, isPlaylistDownloading$,
<>
<BsmImage image={BeatConflict} className="size-24"/>
<div className="text-white font-bold w-fit space-y-1.5 flex flex-col justify-center items-center">
<p>La Playlist est en attente de téléchargment</p>
<p>{t("playlist.playlist-is-waiting-to-download")}</p>
</div>
</>
)
);
}
return (
<>
<BsmImage image={BeatConflict} className="size-24"/>
<div className="text-white font-bold w-fit space-y-1.5 flex flex-col justify-center items-center">
<p>Certaines maps de cette playlist sont manquantes</p>
<BsmButton withBar={false} onClick={installPlaylist} className="rounded-md h-8 flex items-center justify-center px-4" typeColor="primary" text="Télécharger.les.maps.manquantes"/>
<p>{t("playlist.some-playlist-maps-are-missing")}</p>
<BsmButton withBar={false} onClick={installPlaylist} className="rounded-md h-8 flex items-center justify-center px-4" typeColor="primary" text="playlist.download-missing-maps"/>
</div>
</>
);
@@ -5,8 +5,6 @@ import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"
import BeatConflict from "../../../../../../assets/images/apngs/beat-conflict.png";
import { BPList } from "shared/models/playlists/playlist.interface";
// TODO : Translate
export const SyncPlaylistModal: ModalComponent<boolean, BPList[]> = ({ resolver, options: { data }}) => {
const t = useTranslation();
@@ -14,23 +12,23 @@ export const SyncPlaylistModal: ModalComponent<boolean, BPList[]> = ({ resolver,
return (
<form className="max-w-sm text-gray-800 dark:text-gray-200 overflow-hidden">
{data.length === 1 ? (
<h1 className="text-3xl uppercase tracking-wide w-full text-center">Synchroniser la playlist ?</h1>
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("playlist.synchronize-playlist-ask")}</h1>
) : (
<h1 className="text-3xl uppercase tracking-wide w-full text-center">Synchroniser les playlists ?</h1>
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("playlist.synchronize-playlists-ask")}</h1>
)}
<BsmImage className="mx-auto h-24" image={BeatConflict} />
{data.length === 1 ? (
<p className="w-full">{`Est-tu sûr de vouloir synchroniser la playlist "${data.at(0)?.playlistTitle}" ?`}</p>
<p className="w-full">{t("playlist.synchronize-playlist-desc", { playlistTitle: data.at(0)?.playlistTitle })}</p>
) : (
<p className="w-full">{`Est-tu sûr de vouloir synchroniser les ${data.length} playlists ?`}</p>
<p className="w-full">{t("playlist.synchronize-playlists-desc", { nb: `${data.length ?? 0}` })}</p>
)}
<p className="w-full py-2 italic text-sm">Cette action met à jour les playlists et télécharge les maps manquantes; cela peut durer plusieurs minutes.</p>
<p className="w-full py-2 italic text-sm">{t("playlist.synchronize-playlist-tip")}</p>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-2">
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.COMPLETED })} withBar={false} text="Synchroniser" />
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.COMPLETED })} withBar={false} text="playlist.synchronize" />
</div>
</form>
);
@@ -5,27 +5,28 @@ import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
import Tippy from "@tippyjs/react";
export const UnlinkPlaylistModal: ModalComponent<boolean> = ({ resolver }) => {
const t = useTranslation();
const [keepMaps, setKeepMaps] = useState(true);
// TODO : Translate
return (
<form className="text-gray-800 dark:text-gray-200">
<h1 className="text-3xl uppercase tracking-wide w-full text-center">Délier les playlists</h1>
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("playlist.unlink-playlists")}</h1>
<BsmImage className="mx-auto h-24" image={BeatConflict} />
<p className="max-w-sm w-full">Attention, délier les playlists ne permettra plus l'utilisation des playlists paratagées pour cette version.</p>
<p className="max-w-sm w-full">{t("playlist.unlink-playlist-desc")}</p>
<div className="relative h-5 flex my-3 items-center">
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1.5 z-[1]" checked={keepMaps} onChange={setKeepMaps} />
<span className="text-sm mb-0.5 cursor-help" title="Conserver les playlists créera une copie des playlists partagées pour la version actuelle. Dans le cas contraire, aucune playlist ne sera conservée pour cette version.">
Conserver les playlists
</span>
<Tippy content={t("playlist.unlink-keep-playlists-tip")} placement="top" theme="default">
<span className="text-sm mb-0.5 cursor-help">
{t("playlist.keep-playlists")}
</span>
</Tippy>
</div>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4">
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
<BsmButton typeColor="error" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.COMPLETED, data: keepMaps })} withBar={false} text="Délier les playlists" />
<BsmButton typeColor="error" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.COMPLETED, data: keepMaps })} withBar={false} text="playlist.unlink-playlists" />
</div>
</form>
);
+2 -3
View File
@@ -3,7 +3,7 @@ import { useService } from "./use-service.hook";
import { useObservable } from "./use-observable.hook";
export function useTranslation(): (translationKey: string, args?: Record<string, string>) => string {
const i18nService = useService(I18nService);
useObservable(() => i18nService.currentLanguage$);
@@ -12,7 +12,6 @@ export function useTranslation(): (translationKey: string, args?: Record<string,
if (!key) {
return key;
}
const tranlatables = key.split(" ");
return tranlatables.map(key => i18nService.translate(key, args)).join(" ");
return i18nService.translate(key, args);
};
}