feat: react 19

fixes
This commit is contained in:
5rahim
2026-04-14 14:31:21 +02:00
parent b521dae282
commit 2fdeb35912
47 changed files with 1598 additions and 6318 deletions

View File

@@ -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"

View File

@@ -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>",

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -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()));
}

View File

@@ -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),

View File

@@ -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] })

View File

@@ -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...")
},
})

View File

@@ -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]))

View 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]))

View File

@@ -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]))

View File

@@ -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)

View File

@@ -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

View File

@@ -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()
/**

View File

@@ -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

View File

@@ -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>;

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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([])

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)
}

View 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)
}

View File

@@ -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

View File

@@ -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] })
})()
},

View File

@@ -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
}

View File

@@ -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(() => {

View File

@@ -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])
}

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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>,

View File

@@ -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(" "),

View File

@@ -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"

View File

@@ -1 +0,0 @@
export * from "./currency-input"

View File

@@ -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">

View File

@@ -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}
/>
)

View File

@@ -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,

View File

@@ -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>>>>
/**

View File

@@ -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>;

View File

@@ -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) => {

View File

@@ -90,5 +90,5 @@ export function useMainTab(): boolean {
}
}, [socket])
return isMainTab
return false
}

View File

@@ -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,

View File

@@ -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,