From 7562b07832ff27856b59bdd7dae5c7ec13609a82 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Tue, 28 May 2024 22:10:02 +0200 Subject: [PATCH] [feature-107] create generic component for virtual scrolling --- .../maps/local-maps-list-panel.component.tsx | 122 +++++++----------- .../maps/map-item.component.tsx | 36 +++++- .../maps/maps-row.component.tsx | 87 ------------- .../download-maps-modal.component.tsx | 3 +- .../bsv-playlist-details-modal.component.tsx | 3 +- ...local-playlist-details-modal.component.tsx | 3 +- .../playlist-header-state.component.tsx | 2 - .../virtual-scroll/virtual-row.component.tsx | 24 ++++ .../virtual-scroll.component.tsx | 71 ++++++++++ src/renderer/models/api/api.model.ts | 4 - 10 files changed, 178 insertions(+), 177 deletions(-) delete mode 100644 src/renderer/components/maps-playlists-panel/maps/maps-row.component.tsx create mode 100644 src/renderer/components/shared/virtual-scroll/virtual-row.component.tsx create mode 100644 src/renderer/components/shared/virtual-scroll/virtual-scroll.component.tsx delete mode 100644 src/renderer/models/api/api.model.ts diff --git a/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx b/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx index afd30d32..74977b0c 100644 --- a/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx @@ -1,13 +1,11 @@ import { MapsManagerService } from "renderer/services/maps-manager.service"; import { BSVersion } from "shared/bs-version.interface"; -import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState } from "react"; +import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useState } from "react"; import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; import { Subscription, BehaviorSubject } from "rxjs"; import { MapFilter, MapTag } from "shared/models/maps/beat-saver.model"; import { MapsDownloaderService } from "renderer/services/maps-downloader.service"; -import { VariableSizeList } from "react-window"; -import { MapsRow } from "./maps-row.component"; -import { debounceTime, last, tap } from "rxjs/operators"; +import { last, tap } from "rxjs/operators"; import { useTranslation } from "renderer/hooks/use-translation.hook"; import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png"; import { BsmImage } from "../../shared/bsm-image.component"; @@ -21,6 +19,8 @@ import { BsContentLoader } from "renderer/components/shared/bs-content-loader.co import { InstalledMapsContext } from "../maps-playlists-panel.component"; import { useObservable } from "renderer/hooks/use-observable.hook"; import equal from "fast-deep-equal"; +import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component"; +import { MapItem, extractMapDiffs } from "./map-item.component"; type Props = { version: BSVersion; @@ -37,14 +37,10 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa const t = useTranslation(); - const ref = useRef(null); - const {maps$, setMaps} = useContext(InstalledMapsContext); const maps = useObservable(() => maps$, undefined); const [subs] = useState([]); - const [selectedMaps$] = useState(new BehaviorSubject([])); - const [itemPerRow, setItemPerRow] = useState(2); - const [listHeight, setListHeight] = useState(0); + const [selectedMaps, setSelectedMaps] = useState([]); const isActiveOnce = useChangeUntilEqual(isActive, { untilEqual: true }); const [linked, setLinked] = useState(false); @@ -54,20 +50,20 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa forwardRef, () => ({ deleteMaps() { - const mapsToDelete = selectedMaps$.value.length === 0 ? maps : selectedMaps$.value; + const mapsToDelete = selectedMaps.length === 0 ? maps : selectedMaps; mapsManager.deleteMaps(mapsToDelete, version).then(res => { if (!res) { return; } removeMapsFromList(mapsToDelete); - selectedMaps$.next([]); + setSelectedMaps([]); }); }, exportMaps() { - mapsManager.exportMaps(version, selectedMaps$.value); + mapsManager.exportMaps(version, selectedMaps); } }), - [selectedMaps$.value, maps, version] + [selectedMaps, maps, version] ); useOnUpdate(() => { @@ -104,36 +100,6 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa }, [isActiveOnce, version, maps]) - useEffect(() => { - if (!isActiveOnce) { - return; - } - - const updateItemPerRow = (listWidth: number) => { - const newPerRow = Math.min(Math.floor(listWidth / 400), 3); - if (newPerRow === itemPerRow) { - return; - } - setItemPerRow(newPerRow); - }; - - const heightObserver$ = new BehaviorSubject(0); - - const observer = new ResizeObserver(() => { - updateItemPerRow(ref.current?.clientWidth || 0); - heightObserver$.next(ref.current?.clientHeight || 0); - }); - - observer.observe(ref.current); - - const sub = heightObserver$.pipe(debounceTime(100)).subscribe(setListHeight); - - return () => { - observer.disconnect(); - sub.unsubscribe(); - }; - }, [isActiveOnce, itemPerRow]); - const loadMaps = () => { setMaps(null); loadPercent$.next(0); @@ -157,8 +123,8 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa const filtredMaps = maps.filter(map => !mapsToRemove.some(toDeleteMaps => map.hash === toDeleteMaps.hash)); setMaps(filtredMaps); - const filtredSelectedMaps = selectedMaps$.value.filter(map => !mapsToRemove.some(toDeleteMaps => map.hash === toDeleteMaps.hash)); - selectedMaps$.next(filtredSelectedMaps); + const filtredSelectedMaps = selectedMaps.filter(map => !mapsToRemove.some(toDeleteMaps => map.hash === toDeleteMaps.hash)); + setSelectedMaps(filtredSelectedMaps); }; const handleDelete = useCallback( @@ -175,7 +141,7 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa const onMapSelected = useCallback( (map: BsmLocalMap) => { - const mapsCopy = [...selectedMaps$.value]; + const mapsCopy = [...selectedMaps]; if (mapsCopy.some(selectedMap => selectedMap.hash === map.hash)) { const i = mapsCopy.findIndex(selectedMap => selectedMap.hash === map.hash); mapsCopy.splice(i, 1); @@ -183,9 +149,9 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa mapsCopy.push(map); } - selectedMaps$.next(mapsCopy); + setSelectedMaps(mapsCopy) }, - [selectedMaps$.value] + [selectedMaps] ); const isMapFitFilter = (map: BsmLocalMap): boolean => { @@ -356,35 +322,17 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa return true; }; - const preppedMaps: BsmLocalMap[][] = (() => { + const preppedMaps: BsmLocalMap[] = (() => { if (!maps) { return []; } - const res: BsmLocalMap[][] = []; - let mapsRow: BsmLocalMap[] = []; - - for (const map of maps) { - if (!isMapFitFilter(map)) { - continue; - } - if (mapsRow.length === itemPerRow) { - res.push(mapsRow); - mapsRow = []; - } - mapsRow.push(map); - } - - if (mapsRow.length) { - res.push(mapsRow); - } - - return res; + return maps.filter(isMapFitFilter); })(); if (!maps) { return ( -
+
); @@ -392,7 +340,7 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa if (!maps.length) { return ( -
+
{t("pages.version-viewer.maps.tabs.maps.empty-maps.text")} @@ -412,10 +360,34 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa } return ( -
- 108} itemCount={preppedMaps.length} itemData={preppedMaps} layout="vertical" style={{ scrollbarGutter: "stable both-edges" }} itemKey={(i, data) => data[i].map(map => map.hash).join()}> - {props => } - -
+ ( + selectedMap.hash === map.hash)} + diffs={extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })} + mapId={map.songDetails?.id} + ranked={map.songDetails?.ranked} + autorId={map.songDetails?.uploader.id} + likes={map.songDetails?.upVotes} + createdAt={map.songDetails?.uploadedAt} + onDelete={handleDelete} + onSelected={onMapSelected} + callBackParam={map} + /> + )}/> ); }); diff --git a/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx b/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx index fab4705b..8fd99d6f 100644 --- a/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx @@ -20,7 +20,7 @@ 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 { BsvMapDetail, RawMapInfoData, SongDetailDiffCharactertistic, SongDetails, SongDiffName } from "shared/models/maps"; import { useConstant } from "renderer/hooks/use-constant.hook"; import { CalendarDateTime, getLocalTimeZone } from "@internationalized/date"; @@ -73,8 +73,6 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl, const songPlaying = useObservable(() => audioPlayer.playing$.pipe(map(playing => playing && audioPlayer.src === songUrl))); - console.log("aaa", autorId); - 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; @@ -340,3 +338,35 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl, ); }, equal); + +export function extractMapDiffs({rawMapInfo, songDetails, bsvMap}: {rawMapInfo?: RawMapInfoData, songDetails?: SongDetails, bsvMap?: BsvMapDetail}): Map { + const res = new Map(); + if (bsvMap && bsvMap.versions?.at(0)?.diffs) { + bsvMap.versions.at(0).diffs.forEach(diff => { + const arr = res.get(diff.characteristic) || []; + arr.push({ name: diff.difficulty, type: diff.difficulty, stars: diff.stars }); + res.set(diff.characteristic, arr); + }); + return res; + } + + if (songDetails?.difficulties) { + songDetails?.difficulties.forEach(diff => { + const arr = res.get(diff.characteristic) || []; + const diffName = rawMapInfo._difficultyBeatmapSets.find(set => set._beatmapCharacteristicName === diff.characteristic)._difficultyBeatmaps.find(rawDiff => rawDiff._difficulty === diff.difficulty)?._customData?._difficultyLabel || diff.difficulty; + arr.push({ name: diffName, type: diff.difficulty, stars: diff.stars }); + res.set(diff.characteristic, arr); + }); + return res; + } + + rawMapInfo._difficultyBeatmapSets.forEach(set => { + set._difficultyBeatmaps.forEach(diff => { + const arr = res.get(set._beatmapCharacteristicName) || []; + arr.push({ name: diff._customData?._difficultyLabel || diff._difficulty, type: diff._difficulty, stars: null }); + res.set(set._beatmapCharacteristicName, arr); + }); + }); + + return res; +} diff --git a/src/renderer/components/maps-playlists-panel/maps/maps-row.component.tsx b/src/renderer/components/maps-playlists-panel/maps/maps-row.component.tsx deleted file mode 100644 index 68e66c34..00000000 --- a/src/renderer/components/maps-playlists-panel/maps/maps-row.component.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { CSSProperties, memo } from "react"; -import { useObservable } from "renderer/hooks/use-observable.hook"; -import { distinctUntilChanged, map } from "rxjs/operators"; -import { BehaviorSubject } from "rxjs"; -import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; -import { ParsedMapDiff, MapItem } from "./map-item.component"; -import equal from "fast-deep-equal/es6"; -import { SongDetailDiffCharactertistic, SongDetails } from "shared/models/maps/song-details-cache/song-details-cache.model"; -import { BsvMapDetail, RawMapInfoData } from "shared/models/maps"; - -type Props = { - maps: BsmLocalMap[]; - style?: CSSProperties; - onMapDelete: (map: BsmLocalMap) => void; - onMapSelect: (map: BsmLocalMap) => void; - selectedMaps$: BehaviorSubject; -}; - -export const MapsRow = memo(({ maps, style, selectedMaps$, onMapSelect, onMapDelete }: Props) => { - const selectedMaps = useObservable(() => selectedMaps$.pipe( - map(selectedMaps => selectedMaps.filter(selected => maps.some(map => map.hash === selected.hash))), - distinctUntilChanged(equal), - ), []); - - const renderMapItem = (map: BsmLocalMap) => { - - return selected.hash === map.hash)} - diffs={extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })} - mapId={map.songDetails?.id} - ranked={map.songDetails?.ranked} - autorId={map.songDetails?.uploader.id} - likes={map.songDetails?.upVotes} - createdAt={map.songDetails?.uploadedAt} - onDelete={onMapDelete} - onSelected={onMapSelect} - callBackParam={map} - />; - }; - - return ( -
    - {maps?.map(renderMapItem)} -
- ); -}, equal); - -export function extractMapDiffs({rawMapInfo, songDetails, bsvMap}: {rawMapInfo?: RawMapInfoData, songDetails?: SongDetails, bsvMap?: BsvMapDetail}): Map { - const res = new Map(); - if (bsvMap && bsvMap.versions?.at(0)?.diffs) { - bsvMap.versions.at(0).diffs.forEach(diff => { - const arr = res.get(diff.characteristic) || []; - arr.push({ name: diff.difficulty, type: diff.difficulty, stars: diff.stars }); - res.set(diff.characteristic, arr); - }); - return res; - } - - if (songDetails?.difficulties) { - songDetails?.difficulties.forEach(diff => { - const arr = res.get(diff.characteristic) || []; - const diffName = rawMapInfo._difficultyBeatmapSets.find(set => set._beatmapCharacteristicName === diff.characteristic)._difficultyBeatmaps.find(rawDiff => rawDiff._difficulty === diff.difficulty)?._customData?._difficultyLabel || diff.difficulty; - arr.push({ name: diffName, type: diff.difficulty, stars: diff.stars }); - res.set(diff.characteristic, arr); - }); - return res; - } - - rawMapInfo._difficultyBeatmapSets.forEach(set => { - set._difficultyBeatmaps.forEach(diff => { - const arr = res.get(set._beatmapCharacteristicName) || []; - arr.push({ name: diff._customData?._difficultyLabel || diff._difficulty, type: diff._difficulty, stars: null }); - res.set(set._beatmapCharacteristicName, arr); - }); - }); - - return res; -} diff --git a/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx b/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx index f3eea5b1..1aaadc33 100644 --- a/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx @@ -1,7 +1,7 @@ import { motion } from "framer-motion"; import { useCallback, useEffect, useRef, useState } from "react"; import { FilterPanel } from "renderer/components/maps-playlists-panel/maps/filter-panel.component"; -import { MapItem } from "renderer/components/maps-playlists-panel/maps/map-item.component"; +import { MapItem, extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/map-item.component"; import { BsmButton } from "renderer/components/shared/bsm-button.component"; import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component"; import { BsmSelect, BsmSelectOption } from "renderer/components/shared/bsm-select.component"; @@ -19,7 +19,6 @@ import { useTranslation } from "renderer/hooks/use-translation.hook"; import { OsDiagnosticService } from "renderer/services/os-diagnostic.service"; import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; import { useService } from "renderer/hooks/use-service.hook"; -import { extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/maps-row.component"; import { useConstant } from "renderer/hooks/use-constant.hook"; import { getLocalTimeZone, parseAbsolute, toCalendarDateTime } from "@internationalized/date"; diff --git a/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/bsv-playlist-details-modal.component.tsx b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/bsv-playlist-details-modal.component.tsx index a217caf4..686fbbb9 100644 --- a/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/bsv-playlist-details-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/bsv-playlist-details-modal.component.tsx @@ -6,8 +6,7 @@ import { Observable } from "rxjs"; import { useObservable } from "renderer/hooks/use-observable.hook"; import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"; import { useState } from "react"; -import { MapItem } from "renderer/components/maps-playlists-panel/maps/map-item.component"; -import { extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/maps-row.component"; +import { MapItem, extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/map-item.component"; import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; import { useService } from "renderer/hooks/use-service.hook"; import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.service"; diff --git a/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component.tsx b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component.tsx index 6f3905fc..71f050b4 100644 --- a/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component.tsx @@ -4,8 +4,7 @@ import { Observable, combineLatest, lastValueFrom, map, switchMap } from "rxjs" import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface" import { BSVersion } from "shared/bs-version.interface"; import { useObservable } from "renderer/hooks/use-observable.hook"; -import { MapItem } from "renderer/components/maps-playlists-panel/maps/map-item.component"; -import { extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/maps-row.component"; +import { MapItem, extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/map-item.component"; import { useService } from "renderer/hooks/use-service.hook"; import { AudioPlayerService } from "renderer/services/audio-player.service"; import { BsmImage } from "renderer/components/shared/bsm-image.component"; diff --git a/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/playlist-header-state.component.tsx b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/playlist-header-state.component.tsx index 939994fe..4515a492 100644 --- a/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/playlist-header-state.component.tsx +++ b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/playlist-header-state.component.tsx @@ -18,8 +18,6 @@ export function PlaylistHeaderState({isPlaylistInQueue$, isPlaylistDownloading$, const isPlaylistDownloading = useObservable(() => isPlaylistDownloading$, false); const isMissingMaps = useObservable(() => isMissingMaps$, false); - console.log(isPlaylistInQueue, isPlaylistDownloading, isMissingMaps); - const renderHeaderContent = () => { if(isPlaylistDownloading) { diff --git a/src/renderer/components/shared/virtual-scroll/virtual-row.component.tsx b/src/renderer/components/shared/virtual-scroll/virtual-row.component.tsx new file mode 100644 index 00000000..a54f9286 --- /dev/null +++ b/src/renderer/components/shared/virtual-scroll/virtual-row.component.tsx @@ -0,0 +1,24 @@ +import equal from "fast-deep-equal"; +import { CSSProperties, memo } from "react"; +import { cn } from "renderer/helpers/css-class.helpers"; + +type Props = { + className?: string; + style?: CSSProperties; + items: T[]; + renderItem: (item: T) => JSX.Element; +} + +function VirtualRowComponent({ className, style, items, renderItem }: Props) { + return ( +
    + {items.map((item) => ( + renderItem(item) + ))} +
+ ); +} + +const typedMemo: (c: T, propsAreEqual?: (prevProps: Readonly

, nextProps: Readonly

) => boolean) => T = memo; + +export const VirtualRow = typedMemo(VirtualRowComponent, equal); diff --git a/src/renderer/components/shared/virtual-scroll/virtual-scroll.component.tsx b/src/renderer/components/shared/virtual-scroll/virtual-scroll.component.tsx new file mode 100644 index 00000000..d68fc126 --- /dev/null +++ b/src/renderer/components/shared/virtual-scroll/virtual-scroll.component.tsx @@ -0,0 +1,71 @@ +import { useLayoutEffect, useRef, useState } from "react"; +import { VariableSizeList } from "react-window"; +import { cn } from "renderer/helpers/css-class.helpers"; +import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; +import { VirtualRow } from "./virtual-row.component"; +import { splitIntoChunk } from "shared/helpers/array.helpers"; +import { useConstant } from "renderer/hooks/use-constant.hook"; +import { BehaviorSubject, debounceTime } from "rxjs"; +import { useObservable } from "renderer/hooks/use-observable.hook"; + +type ClassNames = { + mainDiv?: string; + variableList?: string; + rows?: string; +} + +type Props = { + className?: string; + classNames?: ClassNames; + minItemWidth: number; + maxColumns: number; + minColumns?: number; + itemHeight: number; + items: T[]; + renderItem: (item: T) => JSX.Element; +} + +export function VirtualScroll({ className, classNames, minItemWidth, maxColumns, minColumns, itemHeight, items, renderItem}: Props) { + + console.log("omg les classes", className); + + const ref = useRef(null); + + const [itemPerRow, setItemPerRow] = useState(1); + const [itemsToRender, setItemsToRender] = useState([]); + const listHeight$ = useConstant(() => new BehaviorSubject(0)); + const listHeight = useObservable(() => listHeight$.pipe(debounceTime(100)), 0); + + useLayoutEffect(() => { + const updateItemPerRow = (listWidth: number) => { + if (!listWidth) return; + + const calculatedColumns = Math.floor(listWidth / minItemWidth); + const newColumns = Math.max((minColumns || 1), Math.min(maxColumns, calculatedColumns)); + setItemPerRow(() => newColumns); + }; + + const observer = new ResizeObserver(() => { + updateItemPerRow(ref.current?.clientWidth || 0); + listHeight$.next(ref.current?.clientHeight || 0); + console.log("list height", ref.current?.clientHeight); + }); + + observer.observe(ref.current); + + return () => observer.disconnect(); + }, [minItemWidth, maxColumns]); + + useOnUpdate(() => { + const splitedItems = splitIntoChunk(items, itemPerRow); + setItemsToRender(() => splitedItems); + }, [itemPerRow, items]) + + return ( +

+ i} itemSize={() => itemHeight} itemData={itemsToRender} style={{ scrollbarGutter: "stable both-edges" }}> + {props => } + +
+ ) +} diff --git a/src/renderer/models/api/api.model.ts b/src/renderer/models/api/api.model.ts deleted file mode 100644 index 358866a4..00000000 --- a/src/renderer/models/api/api.model.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ApiResult { - status: number; - data: T; -}