mirror of
https://github.com/5rahim/seanime
synced 2026-04-18 22:24:55 +02:00
feat: react 19
fixes
This commit is contained in:
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
Version = "3.6.0"
|
||||
Version = "3.6.0-alpha.5"
|
||||
VersionName = "Kagero"
|
||||
GcTime = time.Minute * 30
|
||||
ConfigFileName = "config.toml"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "seanime-denshi",
|
||||
"version": "3.6.0",
|
||||
"version": "3.6.0-alpha.5",
|
||||
"description": "Electron-based Desktop client for Seanime",
|
||||
"main": "src/main.js",
|
||||
"author": "5rahim <talkwithrahim@gmail.com>",
|
||||
|
||||
6980
seanime-web/package-lock.json
generated
6980
seanime-web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,11 +16,11 @@
|
||||
"preview": "rsbuild preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.20.0",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/autocomplete": "^6.20.1",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/language": "^6.12.3",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/merge": "^6.11.2",
|
||||
"@codemirror/merge": "^6.12.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
@@ -47,19 +47,19 @@
|
||||
"@rrweb/types": "^2.0.0-alpha.20",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/react-query": "^5.90.20",
|
||||
"@tanstack/react-query-devtools": "^5.91.3",
|
||||
"@tanstack/react-router": "^1.157.18",
|
||||
"@tanstack/react-router-devtools": "^1.157.18",
|
||||
"@tanstack/react-query": "^5.99.0",
|
||||
"@tanstack/react-query-devtools": "^5.99.0",
|
||||
"@tanstack/react-router": "^1.168.21",
|
||||
"@tanstack/react-router-devtools": "^1.166.13",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@total-typescript/ts-reset": "^0.6.1",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@uiw/codemirror-theme-vscode": "^4.25.4",
|
||||
"@uiw/react-codemirror": "^4.25.4",
|
||||
"@uiw/codemirror-theme-vscode": "^4.25.9",
|
||||
"@uiw/react-codemirror": "^4.25.9",
|
||||
"@zag-js/number-input": "^1.33.1",
|
||||
"@zag-js/react": "^1.33.1",
|
||||
"anime4k-webgpu": "^1.0.0",
|
||||
"axios": "^1.13.5",
|
||||
"axios": "^1.15.0",
|
||||
"bottleneck": "^2.19.5",
|
||||
"bytes-iec": "^3.1.1",
|
||||
"chalk": "^5.6.2",
|
||||
@@ -84,39 +84,39 @@
|
||||
"hls.js": "1.5.20",
|
||||
"inflection": "^3.0.2",
|
||||
"install": "^0.13.0",
|
||||
"jassub": "^2.4.1",
|
||||
"jotai": "^2.17.0",
|
||||
"jassub": "^2.4.2",
|
||||
"jotai": "^2.19.1",
|
||||
"jotai-derive": "^0.1.3",
|
||||
"jotai-family": "^1.0.1",
|
||||
"jotai-immer": "^0.4.1",
|
||||
"jotai-optics": "^0.4.0",
|
||||
"jotai-scope": "^0.10.0",
|
||||
"js-cookies": "^1.0.4",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
"js-sha256": "^0.11.1",
|
||||
"json2toml": "^6.1.1",
|
||||
"json2toml": "^6.1.2",
|
||||
"libphonenumber-js": "^1.12.9",
|
||||
"media-captions": "^1.0.4",
|
||||
"memory-cache": "^0.2.0",
|
||||
"motion": "^12.29.2",
|
||||
"motion": "^12.38.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"needle": "^3.3.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"normalize-path": "^3.0.0",
|
||||
"ora": "^8.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"react": "^18",
|
||||
"react": "^19",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-compiler-runtime": "19.0.0-beta-e993439-20250405",
|
||||
"react-cookie": "^8.0.1",
|
||||
"react-cookie": "^8.1.0",
|
||||
"react-currency-input-field": "^3.10.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18",
|
||||
"react-day-picker": "^9.14.0",
|
||||
"react-dom": "^19",
|
||||
"react-draggable": "^4.5.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-hook-form": "^7.71.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-phone-number-input": "^3.4.12",
|
||||
"react-hook-form": "^7.72.1",
|
||||
"react-icons": "^5.6.0",
|
||||
"react-remove-scroll-bar": "^2.3.8",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-use": "^17.6.0",
|
||||
@@ -131,19 +131,17 @@
|
||||
"tailwindcss-animate": "~1.0.7",
|
||||
"tsx": "^4.21.0",
|
||||
"upath": "^2.0.1",
|
||||
"use-debounce": "^10.1.0",
|
||||
"use-debounce": "^10.1.1",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/client-preset": "4.8.2",
|
||||
"@rsbuild/core": "^1.7.3",
|
||||
"@rsbuild/plugin-babel": "^1.1.0",
|
||||
"@rsbuild/core": "^1.7.5",
|
||||
"@rsbuild/plugin-babel": "^1.1.2",
|
||||
"@rsbuild/plugin-node-polyfill": "^1.4.4",
|
||||
"@rsbuild/plugin-react": "^1.4.5",
|
||||
"@rsdoctor/rspack-plugin": "^1.5.2",
|
||||
"@tanstack/router-devtools": "^1.157.18",
|
||||
"@tanstack/router-plugin": "^1.158.1",
|
||||
"@rsbuild/plugin-react": "^1.4.6",
|
||||
"@rsdoctor/rspack-plugin": "^1.5.8",
|
||||
"@tanstack/router-plugin": "^1.167.22",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/google.maps": "^3.58.1",
|
||||
@@ -154,16 +152,15 @@
|
||||
"@types/needle": "^3.3.0",
|
||||
"@types/node": "^22.15.32",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@types/react": "^18.3.23",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260221.1",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260414.1",
|
||||
"autoprefixer": "^10",
|
||||
"babel-plugin-react-compiler": "^19.0.0-beta-e993439-20250405",
|
||||
"encoding": "^0.1.13",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5",
|
||||
"workbox-webpack-plugin": "^7.4.0"
|
||||
"typescript": "^6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2376,7 +2376,7 @@
|
||||
});
|
||||
_scheduledResize;
|
||||
resizeCanvas(width, height) {
|
||||
if (width <= 0 || height <= 0)
|
||||
if (!width || !height)
|
||||
return;
|
||||
this._scheduledResize = { width, height };
|
||||
}
|
||||
@@ -2390,6 +2390,8 @@
|
||||
if (!this.ctx)
|
||||
throw new Error("Could not get 2D context");
|
||||
}
|
||||
// not supported
|
||||
// https://issues.chromium.org/u/1/issues/40910142
|
||||
setColorMatrix(subtitleColorSpace, videoColorSpace) {
|
||||
}
|
||||
// this is horribly inefficient, but it's a fallback for systems without a GPU, this is the least of their problems
|
||||
@@ -2673,7 +2675,7 @@ void main() {
|
||||
}
|
||||
_scheduledResize;
|
||||
resizeCanvas(width, height) {
|
||||
if (width <= 0 || height <= 0)
|
||||
if (!width || !height)
|
||||
return;
|
||||
this._scheduledResize = { width, height };
|
||||
}
|
||||
@@ -3026,7 +3028,7 @@ void main() {
|
||||
}
|
||||
_scheduledResize;
|
||||
resizeCanvas(width, height) {
|
||||
if (width <= 0 || height <= 0)
|
||||
if (!width || !height)
|
||||
return;
|
||||
this._scheduledResize = { width, height };
|
||||
}
|
||||
@@ -3324,6 +3326,11 @@ void main() {
|
||||
ready() {
|
||||
return this._ready;
|
||||
}
|
||||
// this passes a string of track data to libass, be it styles, events etc, which it then processes and adds to the track
|
||||
// useful for streaming subtitles
|
||||
processData(events) {
|
||||
this._wasm.processData(events);
|
||||
}
|
||||
createEvent(event) {
|
||||
_applyKeys(event, this._wasm.getEvent(this._wasm.allocEvent()));
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export default defineConfig({
|
||||
include: /\.(?:jsx|tsx)$/,
|
||||
babelLoaderOptions(opts) {
|
||||
opts.plugins ??= []
|
||||
opts.plugins.push(["babel-plugin-react-compiler", { target: "18" }])
|
||||
opts.plugins.push(["babel-plugin-react-compiler"])
|
||||
},
|
||||
}),
|
||||
].filter(Boolean),
|
||||
|
||||
@@ -46,7 +46,10 @@ export function useLogout() {
|
||||
endpoint: API_ENDPOINTS.AUTH.Logout.endpoint,
|
||||
method: API_ENDPOINTS.AUTH.Logout.methods[0],
|
||||
mutationKey: [API_ENDPOINTS.AUTH.Logout.key],
|
||||
onSuccess: async () => {
|
||||
onSuccess: async data => {
|
||||
if (data) {
|
||||
setServerStatus(data)
|
||||
}
|
||||
toast.success("Successfully logged out")
|
||||
await queryClient.invalidateQueries({ queryKey: [API_ENDPOINTS.ANIME_COLLECTION.GetLibraryCollection.key] })
|
||||
await queryClient.invalidateQueries({ queryKey: [API_ENDPOINTS.ANILIST.GetRawAnimeCollection.key] })
|
||||
|
||||
@@ -21,7 +21,9 @@ export function useInstallLatestUpdate() {
|
||||
method: API_ENDPOINTS.RELEASES.InstallLatestUpdate.methods[0],
|
||||
mutationKey: [API_ENDPOINTS.RELEASES.InstallLatestUpdate.key],
|
||||
onSuccess: async (data) => {
|
||||
setServerStatus(data) // Update server status
|
||||
if (data) {
|
||||
setServerStatus(data)
|
||||
}
|
||||
toast.info("Installing update...")
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Anime_LibraryCollection } from "@/api/generated/types"
|
||||
import { Anime_LibraryCollection, Anime_LibraryCollectionEntry } from "@/api/generated/types"
|
||||
import { atom } from "jotai"
|
||||
import { derive } from "jotai-derive"
|
||||
import { atomFamily } from "jotai-family"
|
||||
|
||||
export const animeLibraryCollectionAtom = atom<Anime_LibraryCollection | undefined>(undefined)
|
||||
export const animeLibraryCollectionWithoutStreamsAtom = derive([animeLibraryCollectionAtom], (animeLibraryCollection) => {
|
||||
@@ -16,12 +17,20 @@ export const animeLibraryCollectionWithoutStreamsAtom = derive([animeLibraryColl
|
||||
} as Anime_LibraryCollection
|
||||
})
|
||||
|
||||
export const getAtomicLibraryEntryAtom = atom(get => get(animeLibraryCollectionAtom)?.lists?.length,
|
||||
(get, set, payload: number) => {
|
||||
const lists = get(animeLibraryCollectionAtom)?.lists
|
||||
if (!lists) {
|
||||
return undefined
|
||||
}
|
||||
return lists.flatMap(n => n.entries)?.filter(Boolean).find(n => n.mediaId === payload)
|
||||
},
|
||||
)
|
||||
const animeLibraryEntryIndexAtom = atom<Record<number, Anime_LibraryCollectionEntry>>((get) => {
|
||||
const index: Record<number, Anime_LibraryCollectionEntry> = {}
|
||||
|
||||
get(animeLibraryCollectionAtom)?.lists?.forEach(list => {
|
||||
list.entries?.forEach(entry => {
|
||||
if (!entry) {
|
||||
return
|
||||
}
|
||||
|
||||
index[entry.mediaId] = entry
|
||||
})
|
||||
})
|
||||
|
||||
return index
|
||||
})
|
||||
|
||||
export const getAnimeLibraryEntryAtom = atomFamily((mediaId: number) => atom((get) => get(animeLibraryEntryIndexAtom)[mediaId]))
|
||||
|
||||
23
seanime-web/src/app/(main)/_atoms/manga-collection.atoms.ts
Normal file
23
seanime-web/src/app/(main)/_atoms/manga-collection.atoms.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Manga_Collection, Manga_CollectionEntry } from "@/api/generated/types"
|
||||
import { atom } from "jotai"
|
||||
import { atomFamily } from "jotai-family"
|
||||
|
||||
export const mangaCollectionAtom = atom<Manga_Collection | undefined>(undefined)
|
||||
|
||||
const mangaCollectionEntryIndexAtom = atom<Record<number, Manga_CollectionEntry>>((get) => {
|
||||
const index: Record<number, Manga_CollectionEntry> = {}
|
||||
|
||||
get(mangaCollectionAtom)?.lists?.forEach(list => {
|
||||
list.entries?.forEach(entry => {
|
||||
if (!entry) {
|
||||
return
|
||||
}
|
||||
|
||||
index[entry.mediaId] = entry
|
||||
})
|
||||
})
|
||||
|
||||
return index
|
||||
})
|
||||
|
||||
export const getMangaCollectionEntryAtom = atomFamily((mediaId: number) => atom((get) => get(mangaCollectionEntryIndexAtom)[mediaId]))
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Anime_Episode } from "@/api/generated/types"
|
||||
import { atom } from "jotai"
|
||||
import { atomFamily } from "jotai/utils"
|
||||
|
||||
export const missingEpisodesAtom = atom<Anime_Episode[]>([])
|
||||
|
||||
@@ -7,3 +8,20 @@ export const missingSilencedEpisodesAtom = atom<Anime_Episode[]>([])
|
||||
|
||||
export const missingEpisodeCountAtom = atom(get => get(missingEpisodesAtom).length)
|
||||
|
||||
const missingEpisodesIndexAtom = atom<Record<number, true>>((get) => {
|
||||
const index: Record<number, true> = {}
|
||||
|
||||
get(missingEpisodesAtom).forEach(episode => {
|
||||
const mediaId = episode.baseAnime?.id
|
||||
if (!mediaId) {
|
||||
return
|
||||
}
|
||||
|
||||
index[mediaId] = true
|
||||
})
|
||||
|
||||
return index
|
||||
})
|
||||
|
||||
export const hasMissingEpisodesAtom = atomFamily((mediaId: number) => atom((get) => !!get(missingEpisodesIndexAtom)[mediaId]))
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Status } from "@/api/generated/types"
|
||||
import { atom } from "jotai"
|
||||
import { atomWithImmer } from "jotai-immer"
|
||||
import { atomWithStorage } from "jotai/utils"
|
||||
|
||||
export const serverStatusAtom = atomWithImmer<Status | undefined>(undefined)
|
||||
export const serverStatusAtom = atom<Status | undefined>(undefined)
|
||||
|
||||
export const isLoginModalOpenAtom = atom(false)
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ type EpisodeCardProps = {
|
||||
episodeNumber?: number
|
||||
progressNumber?: number
|
||||
progressTotal?: number
|
||||
mRef?: React.RefObject<HTMLDivElement>
|
||||
mRef?: React.RefObject<HTMLDivElement | null>
|
||||
hasDiscrepancy?: boolean
|
||||
length?: string | number | null
|
||||
imageClass?: string
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useChangelogTourListener } from "@/app/(main)/_features/tour/changelog-
|
||||
|
||||
import { useAnimeCollectionLoader } from "@/app/(main)/_hooks/anilist-collection-loader"
|
||||
import { useAnimeLibraryCollectionLoader } from "@/app/(main)/_hooks/anime-library-collection-loader"
|
||||
import { useMangaCollectionLoader } from "@/app/(main)/_hooks/manga-collection-loader"
|
||||
import { useMissingEpisodesLoader } from "@/app/(main)/_hooks/missing-episodes-loader"
|
||||
import { useAnimeCollectionListener } from "@/app/(main)/_listeners/anilist-collection.listeners"
|
||||
import { useAuthEventListeners } from "@/app/(main)/_listeners/auth.listeners.ts"
|
||||
@@ -100,6 +101,7 @@ function Loader() {
|
||||
*/
|
||||
useAnimeLibraryCollectionLoader()
|
||||
useAnimeCollectionLoader()
|
||||
useMangaCollectionLoader()
|
||||
useMissingEpisodesLoader()
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,8 @@ import { Tooltip } from "@/components/ui/tooltip"
|
||||
import { upath } from "@/lib/helpers/upath"
|
||||
import { ContextMenuGroup } from "@radix-ui/react-context-menu"
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai"
|
||||
import { pascalCase } from "pascal-case"
|
||||
import camelCase from "lodash/camelCase"
|
||||
import upperFirst from "lodash/upperFirst"
|
||||
import React, { memo } from "react"
|
||||
import { BiChevronDown, BiChevronRight, BiFolder, BiListCheck, BiLockOpenAlt, BiSearch } from "react-icons/bi"
|
||||
import { FaRegEdit } from "react-icons/fa"
|
||||
@@ -524,7 +525,7 @@ export function LibraryExplorer() {
|
||||
"animate-pulse",
|
||||
)}
|
||||
>
|
||||
Filter: {!!selectedFilter ? pascalCase(selectedFilter) : ""}
|
||||
Filter: {!!selectedFilter ? upperFirst(camelCase(selectedFilter)) : ""}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
|
||||
@@ -58,7 +58,7 @@ export function MediaCardGrid(props: MediaCardGridProps) {
|
||||
type MediaCardLazyGridProps = {
|
||||
children: React.ReactNode
|
||||
itemCount: number
|
||||
containerRef?: React.RefObject<HTMLElement>
|
||||
containerRef?: React.RefObject<HTMLElement | null>
|
||||
maxCol?: number
|
||||
} & React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ import {
|
||||
AL_BaseManga,
|
||||
Anime_EntryLibraryData,
|
||||
Anime_EntryListData,
|
||||
Anime_LibraryCollectionEntry,
|
||||
Anime_NakamaEntryLibraryData,
|
||||
Manga_EntryListData,
|
||||
} from "@/api/generated/types"
|
||||
import { getAtomicLibraryEntryAtom } from "@/app/(main)/_atoms/anime-library-collection.atoms"
|
||||
import { getAnimeLibraryEntryAtom } from "@/app/(main)/_atoms/anime-library-collection.atoms"
|
||||
import { getMangaCollectionEntryAtom } from "@/app/(main)/_atoms/manga-collection.atoms"
|
||||
import { usePlayNext } from "@/app/(main)/_atoms/playback.atoms"
|
||||
import { ToggleLockFilesButton } from "@/app/(main)/_features/anime-library/_containers/toggle-lock-files-button"
|
||||
import { AnimeEntryCardUnwatchedBadge } from "@/app/(main)/_features/anime/_containers/anime-entry-card-unwatched-badge"
|
||||
@@ -32,7 +34,7 @@ import { AnilistMediaEntryModal } from "@/app/(main)/_features/media/_containers
|
||||
import { useMediaPreviewModal } from "@/app/(main)/_features/media/_containers/media-preview-modal"
|
||||
import { usePlaylistEditorManager } from "@/app/(main)/_features/playlists/lib/playlist-editor-manager"
|
||||
import { useAnilistUserAnimeListData } from "@/app/(main)/_hooks/anilist-collection-loader"
|
||||
import { useMissingEpisodes } from "@/app/(main)/_hooks/missing-episodes-loader"
|
||||
import { useHasMissingEpisodes } from "@/app/(main)/_hooks/missing-episodes-loader"
|
||||
import { useHasTorrentOrDebridInclusion, useServerStatus } from "@/app/(main)/_hooks/use-server-status"
|
||||
import { MangaEntryCardUnreadBadge } from "@/app/(main)/manga/_containers/manga-entry-card-unread-badge"
|
||||
import { SeaLink } from "@/components/shared/sea-link"
|
||||
@@ -40,8 +42,7 @@ import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuTrigger } from "@/components/ui/context-menu"
|
||||
import { useRouter } from "@/lib/navigation"
|
||||
import { useAtom } from "jotai"
|
||||
import { useSetAtom } from "jotai/react"
|
||||
import { useAtomValue, useSetAtom } from "jotai/react"
|
||||
import capitalize from "lodash/capitalize"
|
||||
import React, { useState } from "react"
|
||||
import { BiAddToQueue, BiPlay } from "react-icons/bi"
|
||||
@@ -57,6 +58,8 @@ type MediaEntryCardBaseProps = {
|
||||
showListDataButton?: boolean
|
||||
}
|
||||
|
||||
type MediaEntryCardListData = Anime_EntryListData | Manga_EntryListData
|
||||
|
||||
type MediaEntryCardProps<T extends "anime" | "manga"> = {
|
||||
type: T
|
||||
media: T extends "anime" ? AL_BaseAnime : T extends "manga" ? AL_BaseManga : never
|
||||
@@ -72,6 +75,14 @@ type MediaEntryCardProps<T extends "anime" | "manga"> = {
|
||||
hideReleasingBadge?: boolean
|
||||
} & MediaEntryCardBaseProps
|
||||
|
||||
function useMediaCollectionEntry(type: "anime" | "manga", mediaId: number) {
|
||||
const entryAtom = React.useMemo(() => {
|
||||
return type === "anime" ? getAnimeLibraryEntryAtom(mediaId) : getMangaCollectionEntryAtom(mediaId)
|
||||
}, [mediaId, type])
|
||||
|
||||
return useAtomValue(entryAtom)
|
||||
}
|
||||
|
||||
export function MediaEntryCard<T extends "anime" | "manga">(props: MediaEntryCardProps<T>) {
|
||||
|
||||
const {
|
||||
@@ -80,7 +91,6 @@ export function MediaEntryCard<T extends "anime" | "manga">(props: MediaEntryCar
|
||||
libraryData: _libraryData,
|
||||
nakamaLibraryData,
|
||||
overlay,
|
||||
showListDataButton,
|
||||
showTrailer: _showTrailer,
|
||||
type,
|
||||
withAudienceScore = true,
|
||||
@@ -93,25 +103,45 @@ export function MediaEntryCard<T extends "anime" | "manga">(props: MediaEntryCar
|
||||
const router = useRouter()
|
||||
const serverStatus = useServerStatus()
|
||||
const { hasStreamingEnabled } = useHasTorrentOrDebridInclusion()
|
||||
const missingEpisodes = useMissingEpisodes()
|
||||
|
||||
const prevListDataRef = React.useRef(_listData)
|
||||
const prevLibraryDataRef = React.useRef(_libraryData)
|
||||
|
||||
const [listData, setListData] = useState<Anime_EntryListData | undefined>(_listData)
|
||||
const [libraryData, setLibraryData] = useState<Anime_EntryLibraryData | undefined>(_libraryData)
|
||||
const setActionPopupHover = useSetAtom(__mediaEntryCard_hoveredPopupId)
|
||||
|
||||
const { selectMediaAndOpenEditor } = usePlaylistEditorManager()
|
||||
|
||||
const [__atomicLibraryCollection, getAtomicLibraryEntry] = useAtom(getAtomicLibraryEntryAtom)
|
||||
|
||||
const showLibraryBadge = !!libraryData && !!props.showLibraryBadge
|
||||
|
||||
const mediaId = media.id
|
||||
const mediaEpisodes = (media as AL_BaseAnime)?.episodes
|
||||
const mediaChapters = (media as AL_BaseManga)?.chapters
|
||||
const mediaIsAdult = media?.isAdult
|
||||
const collectionEntry = useMediaCollectionEntry(type, mediaId)
|
||||
const animeListDataFromCollection = useAnilistUserAnimeListData(mediaId, type === "anime" && !_listData)
|
||||
const animeCollectionEntry = type === "anime" ? collectionEntry as Anime_LibraryCollectionEntry | undefined : undefined
|
||||
|
||||
const listData = React.useMemo<MediaEntryCardListData | undefined>(() => {
|
||||
if (_listData) {
|
||||
return _listData
|
||||
}
|
||||
|
||||
if (type === "anime") {
|
||||
return animeListDataFromCollection ?? animeCollectionEntry?.listData
|
||||
}
|
||||
|
||||
return collectionEntry?.listData
|
||||
}, [_listData, type, animeListDataFromCollection, animeCollectionEntry, collectionEntry])
|
||||
|
||||
const libraryData = React.useMemo(() => {
|
||||
if (_libraryData) {
|
||||
return _libraryData
|
||||
}
|
||||
|
||||
if (type !== "anime") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return animeCollectionEntry?.libraryData
|
||||
}, [_libraryData, type, animeCollectionEntry])
|
||||
|
||||
const hasMissingEpisodes = useHasMissingEpisodes(mediaId, type === "anime" && !!libraryData)
|
||||
|
||||
const showLibraryBadge = !!libraryData && !!props.showLibraryBadge
|
||||
|
||||
const showProgressBar = React.useMemo(() => {
|
||||
return !!listData?.progress
|
||||
@@ -136,45 +166,12 @@ export function MediaEntryCard<T extends "anime" | "manga">(props: MediaEntryCar
|
||||
|
||||
const progressTotal = type === "anime" ? (media as AL_BaseAnime)?.episodes : (media as AL_BaseManga)?.chapters
|
||||
|
||||
React.useEffect(() => {
|
||||
if (_listData !== prevListDataRef.current) {
|
||||
prevListDataRef.current = _listData
|
||||
setListData(_listData)
|
||||
}
|
||||
}, [_listData])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (_libraryData !== prevLibraryDataRef.current) {
|
||||
prevLibraryDataRef.current = _libraryData
|
||||
setLibraryData(_libraryData)
|
||||
}
|
||||
}, [_libraryData])
|
||||
|
||||
// Dynamically refresh data when LibraryCollection is updated
|
||||
React.useEffect(() => {
|
||||
const entry = getAtomicLibraryEntry(mediaId)
|
||||
if (!_listData) {
|
||||
setListData(entry?.listData)
|
||||
}
|
||||
if (!_libraryData) {
|
||||
setLibraryData(entry?.libraryData)
|
||||
}
|
||||
}, [__atomicLibraryCollection, mediaId, _listData, _libraryData])
|
||||
|
||||
const listDataFromCollection = useAnilistUserAnimeListData(mediaId)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (listDataFromCollection && !_listData && listDataFromCollection !== listData) {
|
||||
setListData(listDataFromCollection)
|
||||
}
|
||||
}, [listDataFromCollection, _listData, listData])
|
||||
|
||||
const { setPlayNext } = usePlayNext()
|
||||
const handleWatchButtonClicked = React.useCallback(() => {
|
||||
setPlayNext(mediaId, () => {
|
||||
router.push(ANIME_LINK)
|
||||
})
|
||||
}, [listData?.progress, listData?.status, mediaId, ANIME_LINK, setPlayNext, router])
|
||||
}, [mediaId, ANIME_LINK, setPlayNext, router])
|
||||
|
||||
const onPopupMouseEnter = React.useCallback(() => {
|
||||
setActionPopupHover(mediaId)
|
||||
@@ -187,7 +184,6 @@ export function MediaEntryCard<T extends "anime" | "manga">(props: MediaEntryCar
|
||||
const { setPreviewModalMediaId } = useMediaPreviewModal()
|
||||
const { openDirInLibraryExplorer } = useLibraryExplorer()
|
||||
|
||||
const [hoveringTitle, setHoveringTitle] = useState(false)
|
||||
const [isHoveringCard, setIsHoveringCard] = useState(false)
|
||||
const [shouldRenderPopup, setShouldRenderPopup] = useState(false)
|
||||
|
||||
@@ -423,7 +419,7 @@ export function MediaEntryCard<T extends "anime" | "manga">(props: MediaEntryCar
|
||||
score={listData?.score}
|
||||
/>
|
||||
</div>
|
||||
{(type === "anime" && !!libraryData && missingEpisodes.find(n => n.baseAnime?.id === media.id)) && (
|
||||
{(type === "anime" && !!libraryData && hasMissingEpisodes) && (
|
||||
<div
|
||||
data-media-entry-card-body-missing-episodes-badge-container
|
||||
className="absolute z-[10] w-full flex justify-center left-1 bottom-0"
|
||||
|
||||
@@ -195,13 +195,13 @@ function Content(props: { layout: "fixed" | "videocore" }) {
|
||||
function ChatContent(props: {
|
||||
messages: ChatMessage[]
|
||||
currentUserPeerId: string | null | undefined
|
||||
messagesEndRef: React.RefObject<HTMLDivElement>
|
||||
chatContainerRef: React.RefObject<HTMLDivElement>
|
||||
messagesEndRef: React.RefObject<HTMLDivElement | null>
|
||||
chatContainerRef: React.RefObject<HTMLDivElement | null>
|
||||
inputValue: string
|
||||
setInputValue: (value: string) => void
|
||||
handleKeyPress: (e: React.KeyboardEvent) => void
|
||||
isSending: boolean
|
||||
inputRef: React.RefObject<HTMLInputElement>
|
||||
inputRef: React.RefObject<HTMLInputElement | null>
|
||||
handleSendMessage: () => void
|
||||
}) {
|
||||
const {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { UpdateModal } from "@/app/(main)/_features/update/update-modal"
|
||||
import { useAutoDownloaderQueueCount } from "@/app/(main)/_hooks/autodownloader-queue-count"
|
||||
import { useWebsocketMessageListener } from "@/app/(main)/_hooks/handle-websockets"
|
||||
import { useMissingEpisodeCount } from "@/app/(main)/_hooks/missing-episodes-loader"
|
||||
import { useCurrentUser, useServerStatus, useSetServerStatus } from "@/app/(main)/_hooks/use-server-status"
|
||||
import { useCurrentUser, useServerStatus } from "@/app/(main)/_hooks/use-server-status"
|
||||
import { ConfirmationDialog, useConfirmationDialog } from "@/components/shared/confirmation-dialog"
|
||||
import { SeaLink } from "@/components/shared/sea-link"
|
||||
import { AppSidebar, useAppSidebarContext } from "@/components/ui/app-layout"
|
||||
@@ -57,14 +57,7 @@ export function MainSidebar() {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
// Logout
|
||||
const setServerStatus = useSetServerStatus()
|
||||
const { mutate: logout, data, isPending } = useLogout()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isPending) {
|
||||
setServerStatus(data)
|
||||
}
|
||||
}, [isPending, data])
|
||||
const { mutate: logout } = useLogout()
|
||||
|
||||
|
||||
const handleExpandSidebar = () => {
|
||||
@@ -117,7 +110,7 @@ export function MainSidebar() {
|
||||
}
|
||||
|
||||
|
||||
function SidebarNavigation({ isCollapsed, containerRef }: { isCollapsed: boolean, containerRef: React.RefObject<HTMLDivElement> }) {
|
||||
function SidebarNavigation({ isCollapsed, containerRef }: { isCollapsed: boolean, containerRef: React.RefObject<HTMLDivElement | null> }) {
|
||||
const ctx = useAppSidebarContext()
|
||||
const ts = useThemeSettings()
|
||||
const router = useRouter()
|
||||
@@ -266,7 +259,7 @@ function SidebarNavigation({ isCollapsed, containerRef }: { isCollapsed: boolean
|
||||
|
||||
// Overflow logic
|
||||
const [autoUnpinnedIds, setAutoUnpinnedIds] = React.useState<string[]>([])
|
||||
const overflowCheckTimeoutRef = React.useRef<NodeJS.Timeout>()
|
||||
const overflowCheckTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleResize = () => setAutoUnpinnedIds([])
|
||||
|
||||
@@ -31,7 +31,7 @@ export type SeaCommandContextProps = {
|
||||
select: (func?: () => void) => void
|
||||
command: SeaCommand_ParsedCommandProps
|
||||
scrollToTop: () => () => void
|
||||
commandListRef: React.RefObject<HTMLDivElement>
|
||||
commandListRef: React.RefObject<HTMLDivElement | null>
|
||||
router: {
|
||||
pathname: string
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ export function VideoCoreMobileControlBar(props: {
|
||||
const [, setHoveringControlBar] = useAtom(vc_hoveringControlBar)
|
||||
|
||||
const [isSwipingDebounced, setIsSwipingDebounced] = React.useState(false)
|
||||
const sieT = React.useRef<NodeJS.Timeout>()
|
||||
const sieT = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
React.useEffect(() => {
|
||||
if (isSwiping) {
|
||||
setIsSwipingDebounced(true)
|
||||
|
||||
@@ -174,7 +174,7 @@ export function VideoCoreInlineLayout(props: VideoCoreInlineLayoutProps) {
|
||||
// Scroll to selected episode element when the episode list changes (on mount)
|
||||
const episodeListContainerRef = React.useRef<HTMLDivElement>(null)
|
||||
const episodeListViewportRef = React.useRef<HTMLDivElement>(null)
|
||||
const scrollTimeoutRef = React.useRef<NodeJS.Timeout>()
|
||||
const scrollTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const mediaPlayerContainerRef = React.useRef<HTMLDivElement>(null)
|
||||
const contentContainerRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
|
||||
@@ -792,7 +792,7 @@ export function VideoCorePreferencesModal({ isWebPlayer }: { isWebPlayer: boolea
|
||||
|
||||
export function VideoCoreKeybindingController(props: {
|
||||
active: boolean
|
||||
videoRef: React.RefObject<HTMLVideoElement>,
|
||||
videoRef: React.RefObject<HTMLVideoElement | null>,
|
||||
chapterCues: VideoCoreChapterCue[],
|
||||
introEndTime: number | undefined,
|
||||
introStartTime: number | undefined
|
||||
|
||||
@@ -8,7 +8,7 @@ import { vc_miniPlayer } from "./video-core-atoms"
|
||||
|
||||
interface VideoCoreStatsForNerdsProps {
|
||||
playbackInfo: VideoCore_VideoPlaybackInfo | null
|
||||
videoRef: React.RefObject<HTMLVideoElement>
|
||||
videoRef: React.RefObject<HTMLVideoElement | null>
|
||||
}
|
||||
|
||||
interface PerformanceData {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Anime_EntryListData, Nullish } from "@/api/generated/types"
|
||||
import { useGetAnimeCollection } from "@/api/hooks/anilist.hooks"
|
||||
import { __anilist_userAnimeListDataAtom, __anilist_userAnimeMediaAtom } from "@/app/(main)/_atoms/anilist.atoms"
|
||||
import { atom } from "jotai"
|
||||
import { useAtomValue, useSetAtom } from "jotai/react"
|
||||
import { selectAtom } from "jotai/utils"
|
||||
import React from "react"
|
||||
|
||||
const emptyAnimeListDataAtom = atom<Anime_EntryListData | undefined>(undefined)
|
||||
|
||||
/**
|
||||
* @description
|
||||
* - Fetches the Anilist collection
|
||||
@@ -46,8 +50,15 @@ export function useAnilistUserAnime() {
|
||||
return useAtomValue(__anilist_userAnimeMediaAtom)
|
||||
}
|
||||
|
||||
export function useAnilistUserAnimeListData(mId: Nullish<number | string>): Anime_EntryListData | undefined {
|
||||
const data = useAtomValue(__anilist_userAnimeListDataAtom)
|
||||
export function useAnilistUserAnimeListData(mId: Nullish<number | string>, enabled: boolean = true): Anime_EntryListData | undefined {
|
||||
const mediaId = String(mId)
|
||||
const listDataAtom = React.useMemo(() => {
|
||||
if (!enabled) {
|
||||
return emptyAnimeListDataAtom
|
||||
}
|
||||
|
||||
return data[String(mId)]
|
||||
return selectAtom(__anilist_userAnimeListDataAtom, data => data[mediaId])
|
||||
}, [enabled, mediaId])
|
||||
|
||||
return useAtomValue(listDataAtom)
|
||||
}
|
||||
|
||||
27
seanime-web/src/app/(main)/_hooks/manga-collection-loader.ts
Normal file
27
seanime-web/src/app/(main)/_hooks/manga-collection-loader.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useGetMangaCollection } from "@/api/hooks/manga.hooks"
|
||||
import { mangaCollectionAtom } from "@/app/(main)/_atoms/manga-collection.atoms"
|
||||
import { useAtomValue, useSetAtom } from "jotai/react"
|
||||
import React from "react"
|
||||
|
||||
/**
|
||||
* @description
|
||||
* - Fetches the manga collection and sets it in the atom
|
||||
*/
|
||||
export function useMangaCollectionLoader() {
|
||||
|
||||
const setter = useSetAtom(mangaCollectionAtom)
|
||||
|
||||
const { data, status } = useGetMangaCollection()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (status === "success") {
|
||||
setter(data)
|
||||
}
|
||||
}, [data, status])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function useMangaCollection() {
|
||||
return useAtomValue(mangaCollectionAtom)
|
||||
}
|
||||
@@ -1,8 +1,17 @@
|
||||
import { useGetMissingEpisodes } from "@/api/hooks/anime_entries.hooks"
|
||||
import { missingEpisodeCountAtom, missingEpisodesAtom, missingSilencedEpisodesAtom } from "@/app/(main)/_atoms/missing-episodes.atoms"
|
||||
import {
|
||||
hasMissingEpisodesAtom,
|
||||
missingEpisodeCountAtom,
|
||||
missingEpisodesAtom,
|
||||
missingSilencedEpisodesAtom,
|
||||
} from "@/app/(main)/_atoms/missing-episodes.atoms"
|
||||
import { usePathname } from "@/lib/navigation"
|
||||
import { atom } from "jotai"
|
||||
import { useAtomValue, useSetAtom } from "jotai/react"
|
||||
import { useEffect } from "react"
|
||||
import React from "react"
|
||||
|
||||
const emptyHasMissingEpisodesAtom = atom(false)
|
||||
|
||||
export function useMissingEpisodeCount() {
|
||||
return useAtomValue(missingEpisodeCountAtom)
|
||||
@@ -12,6 +21,18 @@ export function useMissingEpisodes() {
|
||||
return useAtomValue(missingEpisodesAtom)
|
||||
}
|
||||
|
||||
export function useHasMissingEpisodes(mediaId: number, enabled: boolean = true) {
|
||||
const missingEpisodesAtom = React.useMemo(() => {
|
||||
if (!enabled) {
|
||||
return emptyHasMissingEpisodesAtom
|
||||
}
|
||||
|
||||
return hasMissingEpisodesAtom(mediaId)
|
||||
}, [enabled, mediaId])
|
||||
|
||||
return useAtomValue(missingEpisodesAtom)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description
|
||||
* - When the user is not on the main page, send a request to get missing episodes
|
||||
|
||||
@@ -31,6 +31,8 @@ export function useAnimeCollectionListener() {
|
||||
onMessage: data => {
|
||||
(async () => {
|
||||
await qc.invalidateQueries({ queryKey: [API_ENDPOINTS.MANGA.GetAnilistMangaCollection.key] })
|
||||
await qc.invalidateQueries({ queryKey: [API_ENDPOINTS.MANGA.GetRawAnilistMangaCollection.key] })
|
||||
await qc.invalidateQueries({ queryKey: [API_ENDPOINTS.MANGA.GetMangaCollection.key] })
|
||||
await qc.invalidateQueries({ queryKey: [API_ENDPOINTS.MANGA.GetMangaEntry.key] })
|
||||
})()
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import React from "react"
|
||||
type RelationsRecommendationsSectionProps = {
|
||||
entry: Nullish<Anime_Entry>
|
||||
details: Nullish<AL_AnimeDetailsById_Media>
|
||||
containerRef?: React.RefObject<HTMLElement>
|
||||
containerRef?: React.RefObject<HTMLElement | null>
|
||||
maxCol?: number
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ export const IMAGE_STATUS = {
|
||||
ERROR: "error",
|
||||
}
|
||||
|
||||
const useImageLoadStatus = (imageRef: React.RefObject<HTMLImageElement>) => {
|
||||
const useImageLoadStatus = (imageRef: React.RefObject<HTMLImageElement | null>) => {
|
||||
const [imageStatus, setImageStatus] = React.useState(IMAGE_STATUS.LOADING)
|
||||
const retries = React.useRef(0)
|
||||
|
||||
@@ -108,11 +108,12 @@ const useImageLoadStatus = (imageRef: React.RefObject<HTMLImageElement>) => {
|
||||
const retry = React.useCallback(() => {
|
||||
retries.current = 0
|
||||
setImageStatus(IMAGE_STATUS.LOADING)
|
||||
const imgSrc = imageRef.current?.src
|
||||
if (!imgSrc) {
|
||||
const image = imageRef.current
|
||||
const imgSrc = image?.src
|
||||
if (!image || !imgSrc) {
|
||||
return
|
||||
}
|
||||
imageRef.current.src = imgSrc
|
||||
image.src = imgSrc
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -13,8 +13,6 @@ export function useDiscordMangaPresence(entry: { media?: AL_BaseManga } | undefi
|
||||
const { mutate } = useSetDiscordMangaActivity()
|
||||
const { mutate: cancelActivity } = useCancelDiscordActivity()
|
||||
|
||||
const prevChapter = React.useRef<any>()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (serverStatus?.isOffline) return
|
||||
if (
|
||||
@@ -35,7 +33,5 @@ export function useDiscordMangaPresence(entry: { media?: AL_BaseManga } | undefi
|
||||
cancelActivity()
|
||||
}
|
||||
}
|
||||
|
||||
prevChapter.current = currentChapter
|
||||
}, [currentChapter, entry])
|
||||
}
|
||||
|
||||
@@ -35,14 +35,15 @@ export function ServerDataWrapper(props: ServerDataWrapperProps) {
|
||||
const serverStatus = useServerStatus()
|
||||
const setServerStatus = useSetServerStatus()
|
||||
const password = useAtomValue(serverAuthTokenAtom)
|
||||
const { data: _serverStatus, isLoading, refetch } = useGetStatus()
|
||||
const { data: queryServerStatus, isLoading, refetch } = useGetStatus()
|
||||
const resolvedServerStatus = serverStatus ?? queryServerStatus
|
||||
|
||||
React.useEffect(() => {
|
||||
if (_serverStatus) {
|
||||
// logger("SERVER").info("Server status", _serverStatus)
|
||||
setServerStatus(_serverStatus)
|
||||
if (queryServerStatus) {
|
||||
logger("SERVER").info("Server status", queryServerStatus)
|
||||
setServerStatus(queryServerStatus)
|
||||
}
|
||||
}, [_serverStatus])
|
||||
}, [queryServerStatus, setServerStatus])
|
||||
|
||||
useWebsocketMessageListener({
|
||||
type: WSEvents.ANILIST_DATA_LOADED,
|
||||
@@ -52,26 +53,21 @@ export function ServerDataWrapper(props: ServerDataWrapperProps) {
|
||||
},
|
||||
})
|
||||
|
||||
const [authenticated, setAuthenticated] = React.useState(false)
|
||||
const authenticated = !resolvedServerStatus?.serverHasPassword || !!password || pathname === "/public/auth"
|
||||
|
||||
React.useEffect(() => {
|
||||
if (serverStatus) {
|
||||
if (serverStatus?.serverHasPassword && !password && pathname !== "/public/auth") {
|
||||
window.location.href = "/public/auth"
|
||||
setAuthenticated(false)
|
||||
console.warn("Redirecting to auth")
|
||||
} else {
|
||||
setAuthenticated(true)
|
||||
}
|
||||
if (resolvedServerStatus?.serverHasPassword && !password && pathname !== "/public/auth") {
|
||||
window.location.href = "/public/auth"
|
||||
console.warn("Redirecting to auth")
|
||||
}
|
||||
}, [serverStatus?.serverHasPassword, password, pathname])
|
||||
}, [resolvedServerStatus?.serverHasPassword, password, pathname])
|
||||
|
||||
// Refetch the server status every 2 seconds if serverReady is false
|
||||
// This is a fallback to the websocket
|
||||
const intervalId = React.useRef<NodeJS.Timeout | null>(null)
|
||||
const intervalId = React.useRef<number | null>(null)
|
||||
React.useEffect(() => {
|
||||
if (!serverStatus?.serverReady) {
|
||||
intervalId.current = setInterval(() => {
|
||||
if (!resolvedServerStatus?.serverReady) {
|
||||
intervalId.current = window.setInterval(() => {
|
||||
logger("Data Wrapper").info("Refetching server status")
|
||||
refetch()
|
||||
}, 2000)
|
||||
@@ -79,17 +75,26 @@ export function ServerDataWrapper(props: ServerDataWrapperProps) {
|
||||
return () => {
|
||||
logger("Data Wrapper").info("Clearing interval")
|
||||
if (intervalId.current) {
|
||||
clearInterval(intervalId.current)
|
||||
window.clearInterval(intervalId.current)
|
||||
intervalId.current = null
|
||||
}
|
||||
}
|
||||
}, [serverStatus?.serverReady])
|
||||
}, [resolvedServerStatus?.serverReady, refetch])
|
||||
|
||||
logger("SERVER").info("logging server status, ", resolvedServerStatus)
|
||||
logger("SERVER").info("logging server status vars", isLoading || !resolvedServerStatus || !authenticated, {
|
||||
isLoading,
|
||||
serverStatus: resolvedServerStatus,
|
||||
authenticated,
|
||||
})
|
||||
|
||||
/**
|
||||
* If the server status is loading or doesn't exist, show the loading overlay
|
||||
*/
|
||||
if (isLoading || !serverStatus || !authenticated) return <LoadingOverlayWithLogo />
|
||||
if (!serverStatus?.serverReady) return <LoadingOverlayWithLogo title="L o a d i n g" />
|
||||
if (isLoading || !resolvedServerStatus || !authenticated) return <LoadingOverlayWithLogo />
|
||||
if (!resolvedServerStatus.serverReady) return <LoadingOverlayWithLogo title="L o a d i n g" />
|
||||
|
||||
const currentServerStatus = resolvedServerStatus
|
||||
|
||||
/**
|
||||
* If the pathname is /auth/callback, show the callback page
|
||||
@@ -99,14 +104,14 @@ export function ServerDataWrapper(props: ServerDataWrapperProps) {
|
||||
/**
|
||||
* If the server status doesn't have settings, show the getting started page
|
||||
*/
|
||||
if (!serverStatus?.settings) {
|
||||
return <GettingStartedPage status={serverStatus} />
|
||||
if (!currentServerStatus.settings) {
|
||||
return <GettingStartedPage status={currentServerStatus} />
|
||||
}
|
||||
|
||||
/**
|
||||
* If the app is updating, show a different screen
|
||||
*/
|
||||
if (serverStatus?.updating) {
|
||||
if (currentServerStatus.updating) {
|
||||
return <div className="container max-w-3xl py-10">
|
||||
<div className="mb-4 flex justify-center w-full">
|
||||
<img src="/seanime-logo.png" alt="logo" className="w-14 h-auto" />
|
||||
@@ -121,11 +126,11 @@ export function ServerDataWrapper(props: ServerDataWrapperProps) {
|
||||
* Check feature flag routes
|
||||
*/
|
||||
|
||||
if (!serverStatus?.mediastreamSettings?.transcodeEnabled && pathname.startsWith("/mediastream")) {
|
||||
if (!currentServerStatus.mediastreamSettings?.transcodeEnabled && pathname.startsWith("/mediastream")) {
|
||||
return <LuffyError title="Transcoding not enabled" />
|
||||
}
|
||||
|
||||
if (!serverStatus?.user && host === "127.0.0.1:43211" && !__isDesktop__) {
|
||||
if (!currentServerStatus.user && host === "127.0.0.1:43211" && !__isDesktop__) {
|
||||
return <div className="container max-w-3xl py-10">
|
||||
<Card className="md:py-10">
|
||||
<AppLayoutStack>
|
||||
@@ -136,8 +141,8 @@ export function ServerDataWrapper(props: ServerDataWrapperProps) {
|
||||
<h3>Welcome!</h3>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const url = serverStatus?.anilistClientId
|
||||
? `https://anilist.co/api/v2/oauth/authorize?client_id=${serverStatus?.anilistClientId}&response_type=token`
|
||||
const url = currentServerStatus.anilistClientId
|
||||
? `https://anilist.co/api/v2/oauth/authorize?client_id=${currentServerStatus.anilistClientId}&response_type=token`
|
||||
: ANILIST_OAUTH_URL
|
||||
window.open(url, "_self")
|
||||
}}
|
||||
@@ -156,7 +161,7 @@ export function ServerDataWrapper(props: ServerDataWrapperProps) {
|
||||
</AppLayoutStack>
|
||||
</Card>
|
||||
</div>
|
||||
} else if (!serverStatus?.user) {
|
||||
} else if (!currentServerStatus.user) {
|
||||
return <div className="container max-w-3xl py-10">
|
||||
<Card className="md:py-10">
|
||||
<AppLayoutStack>
|
||||
|
||||
@@ -83,7 +83,7 @@ export type FileTreeSelectorProps = {
|
||||
getFileValue: (filePreview: any) => string | number
|
||||
hasLikelyMatch: boolean
|
||||
hasOneLikelyMatch: boolean
|
||||
likelyMatchRef: React.RefObject<HTMLDivElement>
|
||||
likelyMatchRef: React.RefObject<HTMLDivElement | null>
|
||||
}
|
||||
|
||||
type FileTreeNodeProps = {
|
||||
@@ -93,7 +93,7 @@ type FileTreeNodeProps = {
|
||||
getFileValue: (filePreview: any) => string | number
|
||||
hasLikelyMatch: boolean
|
||||
hasOneLikelyMatch: boolean
|
||||
likelyMatchRef: React.RefObject<HTMLDivElement>
|
||||
likelyMatchRef: React.RefObject<HTMLDivElement | null>
|
||||
level?: number
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { cn } from "@/components/ui/core/styling"
|
||||
import { useDraggableScroll } from "@/hooks/use-draggable-scroll"
|
||||
import React, { useRef, useState } from "react"
|
||||
import { MdChevronLeft } from "react-icons/md"
|
||||
import { MdChevronRight } from "react-icons/md"
|
||||
import React, { useRef, useState } from "react"
|
||||
import { useIsomorphicLayoutEffect, useUpdateEffect } from "react-use"
|
||||
|
||||
interface SliderProps {
|
||||
@@ -16,7 +16,7 @@ export const Slider: React.FC<SliderProps> = (props) => {
|
||||
|
||||
const { children, onSlideEnd, ...rest } = props
|
||||
|
||||
const ref = useRef<HTMLDivElement>() as React.MutableRefObject<HTMLInputElement>
|
||||
const ref = useRef<HTMLDivElement>(null) as React.MutableRefObject<HTMLDivElement>
|
||||
const { events } = useDraggableScroll(ref, {
|
||||
decayRate: 0.96,
|
||||
safeDisplacement: 15,
|
||||
|
||||
@@ -15,14 +15,14 @@ export const CalendarAnatomy = defineStyleAnatomy({
|
||||
]),
|
||||
months: cva([
|
||||
"UI-Calendar__months",
|
||||
"flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||
"relative flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||
]),
|
||||
month: cva([
|
||||
"UI-Calendar__month",
|
||||
"space-y-4",
|
||||
]),
|
||||
caption: cva([
|
||||
"UI-Calendar__caption",
|
||||
monthCaption: cva([
|
||||
"UI-Calendar__monthCaption",
|
||||
"flex justify-center pt-1 relative items-center",
|
||||
]),
|
||||
captionLabel: cva([
|
||||
@@ -31,79 +31,81 @@ export const CalendarAnatomy = defineStyleAnatomy({
|
||||
]),
|
||||
nav: cva([
|
||||
"UI-Calendar__nav",
|
||||
"space-x-1 flex items-center",
|
||||
"absolute top-0 flex w-full justify-between",
|
||||
]),
|
||||
navButton: cva([
|
||||
"UI-Calendar__navButton",
|
||||
// buttonPrevious: cva([
|
||||
// "UI-Calendar__buttonPrevious",
|
||||
// ButtonAnatomy.root({ size: "sm", intent: "gray-basic" }),
|
||||
|
||||
// ]),
|
||||
// buttonNext: cva([
|
||||
// "UI-Calendar__buttonNext",
|
||||
// ButtonAnatomy.root({ size: "sm", intent: "gray-basic" }),
|
||||
// ]),
|
||||
chevron: cva([
|
||||
"UI-Calendar__chevron",
|
||||
ButtonAnatomy.root({ size: "sm", intent: "gray-basic" }),
|
||||
"relative z-10",
|
||||
]),
|
||||
navButtonPrevious: cva([
|
||||
"UI-Calendar__navButtonPrevious",
|
||||
"absolute left-1",
|
||||
]),
|
||||
navButtonNext: cva([
|
||||
"UI-Calendar__navButtonNext",
|
||||
"absolute right-1",
|
||||
]),
|
||||
table: cva([
|
||||
"UI-Calendar__table",
|
||||
monthGrid: cva([
|
||||
"UI-Calendar__monthGrid",
|
||||
"w-full border-collapse space-y-1",
|
||||
]),
|
||||
headRow: cva([
|
||||
"UI-Calendar__headRow",
|
||||
weekdays: cva([
|
||||
"UI-Calendar__weekdays",
|
||||
"flex",
|
||||
]),
|
||||
headCell: cva([
|
||||
"UI-Calendar__headCell",
|
||||
weekday: cva([
|
||||
"UI-Calendar__weekday",
|
||||
"text-[--muted] rounded-[--radius] w-9 font-normal text-[0.8rem]",
|
||||
]),
|
||||
row: cva([
|
||||
"UI-Calendar__row",
|
||||
week: cva([
|
||||
"UI-Calendar__week",
|
||||
"flex w-full mt-2",
|
||||
]),
|
||||
cell: cva([
|
||||
"UI-Calendar__cell",
|
||||
day: cva([
|
||||
"UI-Calendar__day",
|
||||
"h-9 w-9 text-center text-sm p-0 relative",
|
||||
"[&:has([aria-selected].day-range-end)]:rounded-r-[--radius]",
|
||||
"[&:has([aria-selected].day-outside)]:bg-[--subtle]/50",
|
||||
"[&:has([aria-selected].range_end)]:rounded-r-[--radius]",
|
||||
"[&:has([aria-selected].outside)]:bg-[--subtle]/50",
|
||||
"[&:has([aria-selected])]:bg-[--subtle]",
|
||||
"first:[&:has([aria-selected])]:rounded-l-[--radius]",
|
||||
"last:[&:has([aria-selected])]:rounded-r-[--radius]",
|
||||
"focus-within:relative focus-within:z-20",
|
||||
]),
|
||||
day: cva([
|
||||
"UI-Calendar__day",
|
||||
dayButton: cva([
|
||||
"UI-Calendar__dayButton",
|
||||
"h-9 w-9 p-0 font-normal aria-selected:opacity-100",
|
||||
]),
|
||||
dayRangeEnd: cva([
|
||||
"UI-Calendar__dayRangeEnd",
|
||||
"day-range-end",
|
||||
range_end: cva([
|
||||
"UI-Calendar__range_end",
|
||||
]),
|
||||
daySelected: cva([
|
||||
"UI-Calendar__daySelected",
|
||||
selected: cva([
|
||||
"UI-Calendar__selected",
|
||||
"bg-brand text-white hover:bg-brand hover:text-white",
|
||||
"focus:bg-brand focus:text-white rounded-[--radius] font-semibold",
|
||||
]),
|
||||
dayToday: cva([
|
||||
"UI-Calendar__dayToday",
|
||||
today: cva([
|
||||
"UI-Calendar__today",
|
||||
"bg-[--subtle] text-[--foreground] rounded-[--radius]",
|
||||
]),
|
||||
dayOutside: cva([
|
||||
"UI-Calendar__dayOutside",
|
||||
"day-outside !text-[--muted] opacity-20",
|
||||
outside: cva([
|
||||
"UI-Calendar__outside",
|
||||
"!text-[--muted] opacity-20",
|
||||
"aria-selected:bg-transparent",
|
||||
"aria-selected:opacity-30",
|
||||
]),
|
||||
dayDisabled: cva([
|
||||
"UI-Calendar__dayDisabled",
|
||||
disabled: cva([
|
||||
"UI-Calendar__disabled",
|
||||
"text-[--muted] opacity-30",
|
||||
]),
|
||||
dayRangeMiddle: cva([
|
||||
"UI-Calendar__dayRangeMiddle",
|
||||
range_middle: cva([
|
||||
"UI-Calendar__range_middle",
|
||||
"aria-selected:bg-[--subtle]",
|
||||
"aria-selected:text-[--foreground]",
|
||||
]),
|
||||
dayHidden: cva([
|
||||
"UI-Calendar__dayHidden",
|
||||
hidden: cva([
|
||||
"UI-Calendar__hidden",
|
||||
"invisible",
|
||||
]),
|
||||
})
|
||||
@@ -123,25 +125,23 @@ export function Calendar(props: CalendarProps) {
|
||||
classNames,
|
||||
monthsClass,
|
||||
monthClass,
|
||||
captionClass,
|
||||
monthCaptionClass,
|
||||
captionLabelClass,
|
||||
navClass,
|
||||
navButtonClass,
|
||||
navButtonPreviousClass,
|
||||
navButtonNextClass,
|
||||
tableClass,
|
||||
headRowClass,
|
||||
headCellClass,
|
||||
rowClass,
|
||||
cellClass,
|
||||
chevronClass,
|
||||
monthGridClass,
|
||||
weekdaysClass,
|
||||
weekdayClass,
|
||||
weekClass,
|
||||
dayClass,
|
||||
dayRangeEndClass,
|
||||
daySelectedClass,
|
||||
dayTodayClass,
|
||||
dayOutsideClass,
|
||||
dayDisabledClass,
|
||||
dayRangeMiddleClass,
|
||||
dayHiddenClass,
|
||||
dayButtonClass,
|
||||
range_endClass,
|
||||
selectedClass,
|
||||
todayClass,
|
||||
outsideClass,
|
||||
disabledClass,
|
||||
range_middleClass,
|
||||
hiddenClass,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
@@ -152,29 +152,28 @@ export function Calendar(props: CalendarProps) {
|
||||
classNames={{
|
||||
months: cn(CalendarAnatomy.months(), monthsClass),
|
||||
month: cn(CalendarAnatomy.month(), monthClass),
|
||||
caption: cn(CalendarAnatomy.caption(), captionClass),
|
||||
month_caption: cn(CalendarAnatomy.monthCaption(), monthCaptionClass),
|
||||
caption_label: cn(CalendarAnatomy.captionLabel(), captionLabelClass),
|
||||
nav: cn(CalendarAnatomy.nav(), navClass),
|
||||
nav_button: cn(CalendarAnatomy.navButton(), ButtonAnatomy.root({ size: "sm", intent: "gray-basic" }), navButtonClass),
|
||||
nav_button_previous: cn(CalendarAnatomy.navButtonPrevious(), navButtonPreviousClass),
|
||||
nav_button_next: cn(CalendarAnatomy.navButtonNext(), navButtonNextClass),
|
||||
table: cn(CalendarAnatomy.table(), tableClass),
|
||||
head_row: cn(CalendarAnatomy.headRow(), headRowClass),
|
||||
head_cell: cn(CalendarAnatomy.headCell(), headCellClass),
|
||||
row: cn(CalendarAnatomy.row(), rowClass),
|
||||
cell: cn(CalendarAnatomy.cell(), cellClass),
|
||||
button_previous: cn(CalendarAnatomy.chevron(), chevronClass),
|
||||
button_next: cn(CalendarAnatomy.chevron(), chevronClass),
|
||||
month_grid: cn(CalendarAnatomy.monthGrid(), monthGridClass),
|
||||
weekdays: cn(CalendarAnatomy.weekdays(), weekdaysClass),
|
||||
weekday: cn(CalendarAnatomy.weekday(), weekdayClass),
|
||||
week: cn(CalendarAnatomy.week(), weekClass),
|
||||
day: cn(CalendarAnatomy.day(), dayClass),
|
||||
day_range_end: cn(CalendarAnatomy.dayRangeEnd(), dayRangeEndClass),
|
||||
day_selected: cn(CalendarAnatomy.daySelected(), daySelectedClass),
|
||||
day_today: cn(CalendarAnatomy.dayToday(), dayTodayClass),
|
||||
day_outside: cn(CalendarAnatomy.dayOutside(), dayOutsideClass),
|
||||
day_disabled: cn(CalendarAnatomy.dayDisabled(), dayDisabledClass),
|
||||
day_range_middle: cn(CalendarAnatomy.dayRangeMiddle(), dayRangeMiddleClass),
|
||||
day_hidden: cn(CalendarAnatomy.dayHidden(), dayHiddenClass),
|
||||
day_button: cn(CalendarAnatomy.dayButton(), dayButtonClass),
|
||||
range_end: cn(CalendarAnatomy.range_end(), range_endClass),
|
||||
selected: cn(CalendarAnatomy.selected(), selectedClass),
|
||||
today: cn(CalendarAnatomy.today(), todayClass),
|
||||
outside: cn(CalendarAnatomy.outside(), outsideClass),
|
||||
disabled: cn(CalendarAnatomy.disabled(), disabledClass),
|
||||
range_middle: cn(CalendarAnatomy.range_middle(), range_middleClass),
|
||||
hidden: cn(CalendarAnatomy.hidden(), hiddenClass),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ ...props }) => <svg
|
||||
Chevron: ({ orientation, className, size: _size, disabled: _disabled, ...props }) => <svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
@@ -182,19 +181,8 @@ export function Calendar(props: CalendarProps) {
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6" />
|
||||
</svg>,
|
||||
IconRight: ({ ...props }) => <svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="rotate-180 w-4 h-4"
|
||||
className={cn("w-4 h-4", orientation === "right" && "rotate-180", className)}
|
||||
{...props}
|
||||
>
|
||||
<path d="m15 18-6-6 6-6" />
|
||||
</svg>,
|
||||
|
||||
@@ -155,6 +155,8 @@ export const LazyCarouselContent = React.forwardRef<HTMLDivElement, LazyCarousel
|
||||
{React.Children.map(children, (child, index) => {
|
||||
const isVisible = visibleIndices.has(index)
|
||||
const storedWidth = itemWidths.get(index)
|
||||
const childElement = React.isValidElement<{ containerClassName?: string }>(child) ? child : null
|
||||
const containerClassName = childElement?.props.containerClassName
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -171,8 +173,8 @@ export const LazyCarouselContent = React.forwardRef<HTMLDivElement, LazyCarousel
|
||||
key={!!(child as React.ReactElement)?.key ? (child as React.ReactElement)?.key : index}
|
||||
className={cn(
|
||||
CarouselAnatomy.item({ orientation, gap }),
|
||||
isVisible && React.isValidElement(child) && child.props.containerClassName
|
||||
? child.props.containerClassName.split(" ").filter((cls: string) => cls.includes("basis-")).join(" ")
|
||||
isVisible && containerClassName
|
||||
? containerClassName.split(" ").filter((cls: string) => cls.includes("basis-")).join(" ")
|
||||
: "",
|
||||
)}
|
||||
style={{
|
||||
@@ -183,9 +185,9 @@ export const LazyCarouselContent = React.forwardRef<HTMLDivElement, LazyCarousel
|
||||
}}
|
||||
>
|
||||
{isVisible ? (
|
||||
React.isValidElement(child) && child.props.containerClassName ? (
|
||||
React.cloneElement(child as React.ReactElement, {
|
||||
containerClassName: child.props.containerClassName
|
||||
childElement && containerClassName ? (
|
||||
React.cloneElement(childElement, {
|
||||
containerClassName: containerClassName
|
||||
.split(" ")
|
||||
.filter((cls: string) => !cls.includes("basis-"))
|
||||
.join(" "),
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
import * as React from "react"
|
||||
import CurrencyInputPrimitive from "react-currency-input-field"
|
||||
import { BasicField, BasicFieldOptions, extractBasicFieldProps } from "../basic-field"
|
||||
import { cn } from "../core/styling"
|
||||
import { extractInputPartProps, InputAddon, InputAnatomy, InputContainer, InputIcon, InputStyling } from "../input"
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* CurrencyInput
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
export type CurrencyInputIntlConfig = {
|
||||
/**
|
||||
* e.g. en-US
|
||||
*/
|
||||
locale: string
|
||||
/**
|
||||
* e.g. USD
|
||||
*/
|
||||
currency?: string
|
||||
}
|
||||
|
||||
export type CurrentInputValues = {
|
||||
/**
|
||||
* Value as float or null if empty
|
||||
* e.g. "1.99" -> 1.99 | "" -> null
|
||||
*/
|
||||
float: number | null
|
||||
/**
|
||||
* Value after applying formatting
|
||||
* e.g. "1000000" -> "1,000,0000"
|
||||
*/
|
||||
formatted: string
|
||||
/**
|
||||
* Non formatted value as string
|
||||
*/
|
||||
value: string
|
||||
}
|
||||
|
||||
export type CurrencyInputProps =
|
||||
Omit<React.ComponentPropsWithoutRef<"input">, "size" | "disabled" | "defaultValue"> &
|
||||
InputStyling &
|
||||
BasicFieldOptions & {
|
||||
/**
|
||||
* Allow decimals
|
||||
* @default true
|
||||
*/
|
||||
allowDecimals?: boolean
|
||||
/**
|
||||
* Allow user to enter negative value
|
||||
* @default true
|
||||
*/
|
||||
allowNegativeValue?: boolean
|
||||
/**
|
||||
* Maximum characters the user can enter
|
||||
*/
|
||||
maxLength?: number
|
||||
/**
|
||||
* Limit length of decimals allowed
|
||||
* @default 2
|
||||
*/
|
||||
decimalsLimit?: number
|
||||
/**
|
||||
* Specify decimal scale for padding/trimming
|
||||
* e.g. 1.5 -> 1.50 | 1.234 -> 1.23
|
||||
*/
|
||||
decimalScale?: number
|
||||
/**
|
||||
* Default value if uncontrolled
|
||||
*/
|
||||
defaultValue?: number | string
|
||||
/**
|
||||
* Value will always have the specified length of decimals
|
||||
* e.g. 123 -> 1.23
|
||||
* Note: This formatting only happens onBlur
|
||||
*/
|
||||
fixedDecimalLength?: number
|
||||
/**
|
||||
* Placeholder if there is no value
|
||||
*/
|
||||
placeholder?: string
|
||||
/**
|
||||
* Include a prefix
|
||||
* e.g. £
|
||||
*/
|
||||
prefix?: string
|
||||
/**
|
||||
* Include a suffix
|
||||
* e.g. €
|
||||
*/
|
||||
suffix?: string
|
||||
/**
|
||||
* Incremental value change on arrow down and arrow up key press
|
||||
*/
|
||||
step?: number
|
||||
/**
|
||||
* Separator between integer part and fractional part of value.
|
||||
*/
|
||||
decimalSeparator?: string
|
||||
/**
|
||||
* Separator between thousand, million and billion.
|
||||
*/
|
||||
groupSeparator?: string
|
||||
/**
|
||||
* Disable auto adding separator between values
|
||||
* e.g. 1000 -> 1,000
|
||||
* @default false
|
||||
*/
|
||||
disableGroupSeparators?: boolean
|
||||
/**
|
||||
* Disable abbreviations (m, k, b)
|
||||
* @default false
|
||||
*/
|
||||
disableAbbreviations?: boolean
|
||||
/**
|
||||
* International locale config
|
||||
* e.g. { locale: 'ja-JP', currency: 'JPY' }
|
||||
* Any prefix, groupSeparator or decimalSeparator options passed in will override Intl Locale config
|
||||
*/
|
||||
intlConfig?: CurrencyInputIntlConfig
|
||||
/**
|
||||
* Transform the raw value form the input before parsing
|
||||
*/
|
||||
transformRawValue?: (rawValue: string) => string
|
||||
/**
|
||||
* Callback invoked when value changes
|
||||
*/
|
||||
onValueChange?: (value: (string | undefined), values?: CurrentInputValues) => void
|
||||
}
|
||||
|
||||
export const CurrencyInput = React.forwardRef<HTMLInputElement, CurrencyInputProps>((props, ref) => {
|
||||
|
||||
const [props1, basicFieldProps] = extractBasicFieldProps<CurrencyInputProps>(props, React.useId())
|
||||
|
||||
const [{
|
||||
size,
|
||||
intent,
|
||||
leftAddon,
|
||||
leftIcon,
|
||||
rightAddon,
|
||||
rightIcon,
|
||||
className,
|
||||
/**/
|
||||
value,
|
||||
onValueChange,
|
||||
transformRawValue,
|
||||
intlConfig,
|
||||
allowDecimals,
|
||||
allowNegativeValue,
|
||||
decimalsLimit,
|
||||
decimalScale,
|
||||
disabled,
|
||||
fixedDecimalLength,
|
||||
placeholder,
|
||||
prefix,
|
||||
suffix,
|
||||
step,
|
||||
decimalSeparator,
|
||||
groupSeparator,
|
||||
disableGroupSeparators,
|
||||
disableAbbreviations,
|
||||
defaultValue,
|
||||
...rest
|
||||
}, {
|
||||
inputContainerProps,
|
||||
leftAddonProps,
|
||||
leftIconProps,
|
||||
rightAddonProps,
|
||||
rightIconProps,
|
||||
}] = extractInputPartProps<CurrencyInputProps>({
|
||||
...props1,
|
||||
size: props1.size ?? "md",
|
||||
intent: props1.intent ?? "basic",
|
||||
leftAddon: props1.leftAddon,
|
||||
leftIcon: props1.leftIcon,
|
||||
rightAddon: props1.rightAddon,
|
||||
rightIcon: props1.rightIcon,
|
||||
})
|
||||
|
||||
return (
|
||||
<BasicField{...basicFieldProps}>
|
||||
<InputContainer {...inputContainerProps}>
|
||||
<InputAddon {...leftAddonProps} />
|
||||
<InputIcon {...leftIconProps} />
|
||||
|
||||
<CurrencyInputPrimitive
|
||||
ref={ref}
|
||||
id={basicFieldProps.id}
|
||||
name={basicFieldProps.name}
|
||||
defaultValue={defaultValue}
|
||||
className={cn(
|
||||
"form-input",
|
||||
InputAnatomy.root({
|
||||
size,
|
||||
intent,
|
||||
hasError: !!basicFieldProps.error,
|
||||
isDisabled: !!basicFieldProps.disabled,
|
||||
isReadonly: !!basicFieldProps.readonly,
|
||||
hasRightAddon: !!rightAddon,
|
||||
hasRightIcon: !!rightIcon,
|
||||
hasLeftAddon: !!leftAddon,
|
||||
hasLeftIcon: !!leftIcon,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
disabled={basicFieldProps.disabled || basicFieldProps.readonly}
|
||||
data-disabled={basicFieldProps.disabled}
|
||||
required={basicFieldProps.required}
|
||||
value={value}
|
||||
onValueChange={(value, _, values) => onValueChange?.(value, values)}
|
||||
transformRawValue={transformRawValue}
|
||||
intlConfig={intlConfig}
|
||||
allowDecimals={allowDecimals}
|
||||
allowNegativeValue={allowNegativeValue}
|
||||
decimalsLimit={decimalsLimit}
|
||||
decimalScale={decimalScale}
|
||||
fixedDecimalLength={fixedDecimalLength}
|
||||
placeholder={placeholder}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
step={step}
|
||||
decimalSeparator={decimalSeparator}
|
||||
groupSeparator={groupSeparator}
|
||||
disableGroupSeparators={disableGroupSeparators}
|
||||
disableAbbreviations={disableAbbreviations}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
<InputAddon {...rightAddonProps} />
|
||||
<InputIcon {...rightIconProps} />
|
||||
</InputContainer>
|
||||
</BasicField>
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
CurrencyInput.displayName = "CurrencyInput"
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./currency-input"
|
||||
@@ -3,7 +3,7 @@ import { cva } from "class-variance-authority"
|
||||
import { Day, formatISO, getYear, Locale, setYear } from "date-fns"
|
||||
import { useAtomValue } from "jotai/react"
|
||||
import * as React from "react"
|
||||
import { DayPickerBase } from "react-day-picker"
|
||||
import { PropsBase } from "react-day-picker"
|
||||
import { BasicField, BasicFieldOptions, extractBasicFieldProps } from "../basic-field"
|
||||
import { Calendar } from "../calendar"
|
||||
import { cn, ComponentAnatomy, defineStyleAnatomy } from "../core/styling"
|
||||
@@ -64,7 +64,7 @@ export type DatePickerProps = Omit<React.ComponentPropsWithRef<"button">, "size"
|
||||
* Props to pass to the date picker
|
||||
* @see https://react-day-picker.js.org/api/interfaces/DayPickerBase
|
||||
*/
|
||||
pickerOptions?: Omit<DayPickerBase, "locale">
|
||||
pickerOptions?: Omit<PropsBase, "locale">
|
||||
/**
|
||||
* Ref to the input element
|
||||
*/
|
||||
@@ -199,7 +199,7 @@ export const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>((
|
||||
onSelect={handleOnSelect}
|
||||
locale={locale}
|
||||
initialFocus
|
||||
tableClass="w-auto mx-auto"
|
||||
monthGridClass="w-auto mx-auto"
|
||||
weekStartsOn={weekStartOn as Day}
|
||||
/>
|
||||
<div className="flex justify-center p-1 border-t">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
import { format, Locale } from "date-fns"
|
||||
import * as React from "react"
|
||||
import { DateRange, DayPickerBase } from "react-day-picker"
|
||||
import { DateRange, PropsBase } from "react-day-picker"
|
||||
import { BasicField, BasicFieldOptions, extractBasicFieldProps } from "../basic-field"
|
||||
import { Calendar } from "../calendar"
|
||||
import { cn, ComponentAnatomy, defineStyleAnatomy } from "../core/styling"
|
||||
@@ -58,7 +58,7 @@ export type DateRangePickerProps = Omit<React.ComponentPropsWithRef<"button">, "
|
||||
* Props to pass to the date picker
|
||||
* @see https://react-day-picker.js.org/api/interfaces/DayPickerBase
|
||||
*/
|
||||
pickerOptions?: Omit<DayPickerBase, "locale">
|
||||
pickerOptions?: Omit<PropsBase, "locale">
|
||||
/**
|
||||
* Ref to the input element
|
||||
*/
|
||||
@@ -169,14 +169,14 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
|
||||
|
||||
const Picker = (
|
||||
<Calendar
|
||||
captionLayout="dropdown-buttons"
|
||||
captionLayout="dropdown"
|
||||
mode="range"
|
||||
defaultMonth={date?.from ?? new Date()}
|
||||
selected={date}
|
||||
onSelect={handleOnSelect}
|
||||
locale={locale}
|
||||
initialFocus
|
||||
tableClass="w-auto mx-auto"
|
||||
monthGridClass="w-auto mx-auto"
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Autocomplete, AutocompleteProps } from "../autocomplete"
|
||||
import { BasicFieldOptions } from "../basic-field"
|
||||
import { Checkbox, CheckboxGroup, CheckboxGroupProps, CheckboxProps } from "../checkbox"
|
||||
import { Combobox, ComboboxProps } from "../combobox"
|
||||
import { CurrencyInput, CurrencyInputProps } from "../currency-input"
|
||||
import { DatePicker, DatePickerProps, DateRangePicker, DateRangePickerProps } from "../date-picker"
|
||||
import { NativeSelect, NativeSelectProps } from "../native-select"
|
||||
import { NumberInput, NumberInputProps } from "../number-input"
|
||||
@@ -352,17 +351,6 @@ const RadioCardsField = React.memo(withControlledInput(forwardRef<HTMLButtonElem
|
||||
},
|
||||
)))
|
||||
|
||||
|
||||
const CurrencyInputField = React.memo(withControlledInput(forwardRef<HTMLInputElement, FieldComponent<CurrencyInputProps>>(
|
||||
({ onChange, ...props }, ref) => {
|
||||
return <CurrencyInput
|
||||
{...props}
|
||||
onValueChange={onChange}
|
||||
ref={ref}
|
||||
/>
|
||||
},
|
||||
)))
|
||||
|
||||
const AutocompleteField = React.memo(withControlledInput(forwardRef<HTMLInputElement, FieldComponent<AutocompleteProps>>(
|
||||
({ onChange, ...props }, ref) => {
|
||||
return <Autocomplete
|
||||
@@ -498,7 +486,6 @@ export const Field = createPolymorphicComponent<"div", FieldProps, {
|
||||
Checkbox: typeof CheckboxField,
|
||||
CheckboxGroup: typeof CheckboxGroupField,
|
||||
RadioGroup: typeof RadioGroupField,
|
||||
Currency: typeof CurrencyInputField,
|
||||
Number: typeof NumberField,
|
||||
DatePicker: typeof DatePickerField
|
||||
DateRangePicker: typeof DateRangePickerField
|
||||
@@ -520,7 +507,6 @@ export const Field = createPolymorphicComponent<"div", FieldProps, {
|
||||
Checkbox: CheckboxField,
|
||||
CheckboxGroup: CheckboxGroupField,
|
||||
RadioGroup: RadioGroupField,
|
||||
Currency: CurrencyInputField,
|
||||
Number: NumberField,
|
||||
DatePicker: DatePickerField,
|
||||
DateRangePicker: DateRangePickerField,
|
||||
|
||||
@@ -52,7 +52,7 @@ export type FormProps<Schema extends z.ZodObject<z.ZodRawShape> = z.ZodObject<z.
|
||||
/**
|
||||
* Ref to the form element.
|
||||
*/
|
||||
formRef?: React.RefObject<HTMLFormElement>
|
||||
formRef?: React.RefObject<HTMLFormElement | null>
|
||||
|
||||
children?: MaybeRenderProp<UseFormReturn<NoInfer<z.infer<Schema>>>>
|
||||
/**
|
||||
|
||||
@@ -2,10 +2,10 @@ import * as React from "react"
|
||||
|
||||
type ExtendedProps<Props = {}, OverrideProps = {}> = OverrideProps &
|
||||
Omit<Props, keyof OverrideProps>;
|
||||
type ElementType = keyof JSX.IntrinsicElements | React.JSXElementConstructor<any>;
|
||||
type PropsOf<C extends ElementType> = JSX.LibraryManagedAttributes<C,
|
||||
type ElementType = React.ElementType;
|
||||
type PropsOf<C extends ElementType> = React.JSX.LibraryManagedAttributes<C,
|
||||
React.ComponentPropsWithoutRef<C>>;
|
||||
type ComponentProp<C> = {
|
||||
type ComponentProp<C extends ElementType> = {
|
||||
component?: C;
|
||||
};
|
||||
type InheritedProps<C extends ElementType, Props = {}> = ExtendedProps<PropsOf<C>, Props>;
|
||||
@@ -16,14 +16,14 @@ export type PolymorphicComponentProps<C, Props = {}> = C extends React.ElementTy
|
||||
? InheritedProps<C, Props & ComponentProp<C>> & { ref?: PolymorphicRef<C> }
|
||||
: Props & { component: React.ElementType };
|
||||
|
||||
export function createPolymorphicComponent<ComponentDefaultType,
|
||||
export function createPolymorphicComponent<ComponentDefaultType extends React.ElementType,
|
||||
Props,
|
||||
StaticComponents = Record<string, never>>(component: any) {
|
||||
type ComponentProps<C> = PolymorphicComponentProps<C, Props>;
|
||||
type ComponentProps<C extends React.ElementType> = PolymorphicComponentProps<C, Props>;
|
||||
|
||||
type _PolymorphicComponent = <C = ComponentDefaultType>(
|
||||
type _PolymorphicComponent = <C extends React.ElementType = ComponentDefaultType>(
|
||||
props: ComponentProps<C>,
|
||||
) => React.ReactElement;
|
||||
) => React.ReactElement | null;
|
||||
|
||||
type ComponentProperties = Omit<React.FunctionComponent<ComponentProps<any>>, never>;
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export type ScrollAreaProps =
|
||||
& ComponentAnatomy<typeof ScrollAreaAnatomy> &
|
||||
{
|
||||
orientation?: "vertical" | "horizontal",
|
||||
viewportRef?: React.RefObject<HTMLDivElement>
|
||||
viewportRef?: React.RefObject<HTMLDivElement | null>
|
||||
}
|
||||
|
||||
export const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>((props, ref) => {
|
||||
|
||||
@@ -90,5 +90,5 @@ export function useMainTab(): boolean {
|
||||
}
|
||||
}, [socket])
|
||||
|
||||
return isMainTab
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface ElementMeasurements {
|
||||
y: number
|
||||
}
|
||||
|
||||
export function useMeasureElement(ref: RefObject<HTMLElement>, deps: any[] = []) {
|
||||
export function useMeasureElement<T extends HTMLElement>(ref: RefObject<T | null>, deps: any[] = []) {
|
||||
const [measurements, setMeasurements] = useState<ElementMeasurements>({
|
||||
width: 0,
|
||||
height: 0,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createRouter, RouterProvider } from "@tanstack/react-router"
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { routeTree } from "./routeTree.gen"
|
||||
import "@fontsource-variable/inter"
|
||||
import "@fontsource-variable/inter/index.css"
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
|
||||
Reference in New Issue
Block a user