import { BsmImage } from "../../shared/bsm-image.component"; import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; import { BsmLink } from "../../shared/bsm-link.component"; import { BsmIcon } from "../../svgs/bsm-icon.component"; import { BsmButton } from "../../shared/bsm-button.component"; import { AnimatePresence, motion } from "framer-motion"; import { useState, Fragment, useRef, useMemo } from "react"; import { LinkOpenerService } from "renderer/services/link-opener.service"; import dateFormat from "dateformat"; import { AudioPlayerService } from "renderer/services/audio-player.service"; import { useObservable } from "renderer/hooks/use-observable.hook"; import { map } from "rxjs/operators"; import equal from "fast-deep-equal/es6"; import { BsmBasicSpinner } from "../../shared/bsm-basic-spinner/bsm-basic-spinner.component"; import defaultImage from "../../../../../assets/images/default-version-img.jpg"; import { useTranslation } from "renderer/hooks/use-translation.hook"; import { MAP_DIFFICULTIES_COLORS } from "shared/models/maps/difficulties-colors"; import useDoubleClick from "use-double-click"; import { GlowEffect } from "../../shared/glow-effect.component"; import { useDelayedState } from "renderer/hooks/use-delayed-state.hook"; import { useService } from "renderer/hooks/use-service.hook"; import Tippy from "@tippyjs/react"; import { SongDetailDiffCharactertistic, SongDiffName } from "shared/models/maps"; import { useConstant } from "renderer/hooks/use-constant.hook"; import { CalendarDateTime, getLocalTimeZone } from "@internationalized/date"; import { typedMemo } from "renderer/helpers/typed-memo"; import { Observable, of } from "rxjs"; import { ParsedMapDiff } from "shared/mappers/map/map-item-component-props.mapper"; import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component"; import { BPListDifficulty } from "shared/models/playlists/playlist.interface"; import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; import { cn } from "renderer/helpers/css-class.helpers"; import { sToMs } from "shared/helpers/time.helpers"; import formatDuration from "format-duration"; import { NpsIcon } from "renderer/components/svgs/icons/nps-icon.component"; import { SpeedIcon } from "renderer/components/svgs/icons/speed-icon.component"; export type MapItemComponentProps = { hash: string; title: string; autor: string; songAutor?: string; coverUrl?: string; songUrl?: string; autorId: number; mapId: string; diffs: Map; highlightedDiffs?: BPListDifficulty[]; ranked?: boolean; blRanked?: boolean; bpm?: number; duration: number; likes: number; createdAt: number | CalendarDateTime; selected?: boolean; selected$?: Observable; downloading?: boolean; showOwned?: boolean; isOwned$?: Observable; canOpenMapDetails?: boolean; canOpenAuthorDetails?: boolean; callBackParam: T; onDelete?: (param: T) => void; onDownload?: (param: T) => void; onSelected?: (param: T) => void; onCancelDownload?: (param: T) => void; onDoubleClick?: (param: T) => void; onHighlightedDiffsChange?: (diffs: BPListDifficulty[]) => void; }; export function MapItemComponent ({ hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, highlightedDiffs, ranked, blRanked, bpm, duration, likes, createdAt, selected, selected$, downloading, showOwned, isOwned$, canOpenMapDetails, canOpenAuthorDetails, callBackParam, onDelete, onDownload, onSelected, onCancelDownload, onDoubleClick, onHighlightedDiffsChange }: MapItemComponentProps) { const linkOpener = useService(LinkOpenerService); const audioPlayer = useService(AudioPlayerService); const color = useThemeColor("first-color"); const t = useTranslation(); const ref = useRef(null); const isSelected = useObservable(() => selected$ ?? of(selected), false, [selected$, selected]); const isOwned = useObservable(() => isOwned$ ?? of(showOwned ?? false), false, [isOwned$, showOwned]); const [hovered, setHovered] = useState(false); const [bottomBarHovered, setBottomBarHovered, cancelBottomBarHovered] = useDelayedState(false); const [diffsPanelHovered, setDiffsPanelHovered] = useState(false); const [_highlightedDiffs, setDiffsSelected] = useState(highlightedDiffs ?? []); useDoubleClick({ ref, latency: onDoubleClick ? 200 : 0, onSingleClick: () => onSelected?.(callBackParam), onDoubleClick: () => onDoubleClick?.(callBackParam), }); useOnUpdate(() => { if(!_highlightedDiffs?.length){ return; } onHighlightedDiffsChange?.(_highlightedDiffs); }, [_highlightedDiffs]) const songPlaying = useObservable(() => audioPlayer.playing$.pipe(map(playing => playing && audioPlayer.src === songUrl))); const MAP_DIFFICULTIES = useConstant(() => Object.values(SongDiffName)) const previewUrl = mapId ? `https://allpoland.github.io/ArcViewer/?id=${mapId}` : null; const mapUrl = mapId ? `https://beatsaver.com/maps/${mapId}` : null; const authorUrl = autorId ? `https://beatsaver.com/profile/${autorId}` : null; const likesText = likes ? Intl.NumberFormat(undefined, { notation: "compact" }).format(likes).split(" ").join("") : null; const mapCoverUrl = coverUrl || `https://eu.cdn.beatsaver.com/${hash}.jpg`; const createdDate = useConstant(() => { if(!createdAt){ return null; } const date = typeof createdAt === "number" ? new Date(createdAt * 1000) : createdAt.toDate(getLocalTimeZone()); return dateFormat(date, "d mmm yyyy"); }); const durationText = useMemo(() => { if (!duration) { return null; } const durationMs = sToMs(duration); return formatDuration(durationMs, { leading: true }); }, [duration]); const parseDiffLabel = (diffLabel: string) => { if (MAP_DIFFICULTIES.includes(diffLabel as SongDiffName)) { return t(`maps.difficulties.${diffLabel}`); } return diffLabel; }; const openPreview = () => { if (audioPlayer.playing) { audioPlayer.pause(); } linkOpener.open(previewUrl, true); }; const copyBsr = () => navigator.clipboard.writeText(`!bsr ${mapId}`); const toogleMusic = () => { if (songPlaying) { return audioPlayer.pause(); } if (!audioPlayer.playing && audioPlayer.src === songUrl) { return audioPlayer.resume(); } audioPlayer.play([{ src: songUrl, bpm: bpm ?? 1 }]); }; const bottomBarHoverStart = () => { cancelBottomBarHovered(); setBottomBarHovered(true, diffsPanelHovered || bottomBarHovered ? 0 : 300); }; const bottomBarHoverEnd = () => { cancelBottomBarHovered(); setBottomBarHovered(false, 100); }; const isDiffHightlighted = (diff: {name: string, characteristic: string}) => { if(!_highlightedDiffs?.length){ return false; } return _highlightedDiffs.some(d => d?.name?.toLowerCase() === diff?.name?.toLowerCase() && d?.characteristic?.toLowerCase() === diff?.characteristic?.toLowerCase()); } const diffsPanelHoverStart = () => setDiffsPanelHovered(true); const diffsPanelHoverEnd = () => setDiffsPanelHovered(false); const renderDiffPreview = () => { const diffSets = Array.from(diffs.entries()); if (diffSets.length === 1) { const [, diffSet] = diffSets[0]; return ( <>
{diffSet.map((diff) => ( ))}
); } if (diffSets.length > 1) { return diffSets.map(([diffType, diffSet]) => ( {diffSet.length} )); } return null; }; const handleDiffCheckChange = (diff: BPListDifficulty) => { if(!_highlightedDiffs?.length){ setDiffsSelected([diff]); } const index = _highlightedDiffs?.findIndex(d => d?.name?.toLowerCase() === diff?.name?.toLowerCase() && d?.characteristic?.toLowerCase() === diff?.characteristic?.toLowerCase()); if(index === -1){ setDiffsSelected([..._highlightedDiffs, diff]); } else { setDiffsSelected(_highlightedDiffs.filter((_, i) => i !== index)); } } return ( setHovered(true)} onHoverEnd={() => setHovered(false)} style={{ zIndex: hovered && 5, transform: "translateZ(0) scale(1.0, 1.0)", backfaceVisibility: "hidden" }}> {(diffsPanelHovered || bottomBarHovered) && ( {Array.from(diffs.entries()).map(([charac, diffSet]) => (
    {diffSet.map(({ name, libelle, stars, nps, njs }) => (
  1. {onHighlightedDiffsChange && (
    handleDiffCheckChange({name, characteristic: charac})} />
    )}
    {(() => { if(stars){ return (
    ★ {stars.toFixed(2)}
    ); } if(nps){ return (
    {nps.toFixed(2)}
    ); } if(njs){ return (

    {njs.toFixed(2)}

    ); } return (
    {parseDiffLabel(name)}
    ); })()}
    {parseDiffLabel(libelle)}
  2. ))}
))}
)}
{ e.stopPropagation(); e.preventDefault(); toogleMusic(); }} >

{canOpenMapDetails !== false ? ( {title} ) : ( title )}

{songAutor && t("maps.map-item.by", { songAutor })}

{autor && ( <> {` ${t("maps.map-item.mapped-by")} `} {canOpenAuthorDetails !== false ? ( {autor} ) : ( {autor} )} )}

{likesText && (
{likesText}
)} {durationText && (
{durationText}
)} {createdAt && (
)}
{ranked && (
{t("maps.map-specificities.ranked")}
)} {blRanked && (
{t("maps.map-specificities.ranked")}
)}
{renderDiffPreview()}
{onDelete && !downloading && ( { e.stopPropagation(); onDelete(callBackParam); }} /> )} {onDownload && !downloading && ( { e.stopPropagation(); onDownload(callBackParam); }} /> )} {onCancelDownload && !downloading && ( { e.stopPropagation(); onCancelDownload(callBackParam); }} /> )} {downloading &&
} {previewUrl && ( { e.stopPropagation(); openPreview(); }} /> )} {mapId && ( { e.stopPropagation(); copyBsr(); }} /> )}
); }; export const MapItem = typedMemo(MapItemComponent, equal);