22 changed files with 195 additions and 525 deletions
@ -1,71 +0,0 @@ |
|||||||
import { ReactNode, createContext, useContext, useMemo } from "react"; |
|
||||||
|
|
||||||
import { MWMediaMeta } from "@/backend/metadata/types/mw"; |
|
||||||
import { useStore } from "@/utils/storage"; |
|
||||||
|
|
||||||
import { BookmarkStore } from "./store"; |
|
||||||
import { BookmarkStoreData } from "./types"; |
|
||||||
|
|
||||||
interface BookmarkStoreDataWrapper { |
|
||||||
setItemBookmark(media: MWMediaMeta, bookedmarked: boolean): void; |
|
||||||
getFilteredBookmarks(): MWMediaMeta[]; |
|
||||||
bookmarkStore: BookmarkStoreData; |
|
||||||
} |
|
||||||
|
|
||||||
const BookmarkedContext = createContext<BookmarkStoreDataWrapper>({ |
|
||||||
setItemBookmark: () => {}, |
|
||||||
getFilteredBookmarks: () => [], |
|
||||||
bookmarkStore: { |
|
||||||
bookmarks: [], |
|
||||||
}, |
|
||||||
}); |
|
||||||
|
|
||||||
function getBookmarkIndexFromMedia( |
|
||||||
bookmarks: MWMediaMeta[], |
|
||||||
media: MWMediaMeta |
|
||||||
): number { |
|
||||||
const a = bookmarks.findIndex((v) => v.id === media.id); |
|
||||||
return a; |
|
||||||
} |
|
||||||
|
|
||||||
export function BookmarkContextProvider(props: { children: ReactNode }) { |
|
||||||
const [bookmarkStorage, setBookmarked] = useStore(BookmarkStore); |
|
||||||
|
|
||||||
const contextValue = useMemo( |
|
||||||
() => ({ |
|
||||||
setItemBookmark(media: MWMediaMeta, bookmarked: boolean) { |
|
||||||
setBookmarked((data: BookmarkStoreData): BookmarkStoreData => { |
|
||||||
let bookmarks = [...data.bookmarks]; |
|
||||||
bookmarks = bookmarks.filter((v) => v.id !== media.id); |
|
||||||
if (bookmarked) bookmarks.push({ ...media }); |
|
||||||
return { |
|
||||||
bookmarks, |
|
||||||
}; |
|
||||||
}); |
|
||||||
}, |
|
||||||
getFilteredBookmarks() { |
|
||||||
return [...bookmarkStorage.bookmarks]; |
|
||||||
}, |
|
||||||
bookmarkStore: bookmarkStorage, |
|
||||||
}), |
|
||||||
[bookmarkStorage, setBookmarked] |
|
||||||
); |
|
||||||
|
|
||||||
return ( |
|
||||||
<BookmarkedContext.Provider value={contextValue}> |
|
||||||
{props.children} |
|
||||||
</BookmarkedContext.Provider> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
export function useBookmarkContext() { |
|
||||||
return useContext(BookmarkedContext); |
|
||||||
} |
|
||||||
|
|
||||||
export function getIfBookmarkedFromPortable( |
|
||||||
bookmarks: MWMediaMeta[], |
|
||||||
media: MWMediaMeta |
|
||||||
): boolean { |
|
||||||
const bookmarked = getBookmarkIndexFromMedia(bookmarks, media); |
|
||||||
return bookmarked !== -1; |
|
||||||
} |
|
@ -1,92 +0,0 @@ |
|||||||
import { ReactNode, createContext, useContext, useMemo } from "react"; |
|
||||||
|
|
||||||
import { LangCode } from "@/setup/iso6391"; |
|
||||||
import { useStore } from "@/utils/storage"; |
|
||||||
|
|
||||||
import { SettingsStore } from "./store"; |
|
||||||
import { MWSettingsData } from "./types"; |
|
||||||
|
|
||||||
interface MWSettingsDataSetters { |
|
||||||
setLanguage(language: LangCode): void; |
|
||||||
setCaptionLanguage(language: LangCode): void; |
|
||||||
setCaptionDelay(delay: number): void; |
|
||||||
setCaptionColor(color: string): void; |
|
||||||
setCaptionFontSize(size: number): void; |
|
||||||
setCaptionBackgroundColor(backgroundColor: number): void; |
|
||||||
} |
|
||||||
type MWSettingsDataWrapper = MWSettingsData & MWSettingsDataSetters; |
|
||||||
const SettingsContext = createContext<MWSettingsDataWrapper>(null as any); |
|
||||||
export function SettingsProvider(props: { children: ReactNode }) { |
|
||||||
function enforceRange(min: number, value: number, max: number) { |
|
||||||
return Math.max(min, Math.min(value, max)); |
|
||||||
} |
|
||||||
const [settings, setSettings] = useStore(SettingsStore); |
|
||||||
const context: MWSettingsDataWrapper = useMemo(() => { |
|
||||||
const settingsContext: MWSettingsDataWrapper = { |
|
||||||
...settings, |
|
||||||
setLanguage(language) { |
|
||||||
setSettings((oldSettings) => { |
|
||||||
return { |
|
||||||
...oldSettings, |
|
||||||
language, |
|
||||||
}; |
|
||||||
}); |
|
||||||
}, |
|
||||||
setCaptionLanguage(language) { |
|
||||||
setSettings((oldSettings) => { |
|
||||||
const captionSettings = oldSettings.captionSettings; |
|
||||||
captionSettings.language = language; |
|
||||||
const newSettings = oldSettings; |
|
||||||
return newSettings; |
|
||||||
}); |
|
||||||
}, |
|
||||||
setCaptionDelay(delay: number) { |
|
||||||
setSettings((oldSettings) => { |
|
||||||
const captionSettings = oldSettings.captionSettings; |
|
||||||
captionSettings.delay = enforceRange(-10, delay, 10); |
|
||||||
const newSettings = oldSettings; |
|
||||||
return newSettings; |
|
||||||
}); |
|
||||||
}, |
|
||||||
setCaptionColor(color) { |
|
||||||
setSettings((oldSettings) => { |
|
||||||
const style = oldSettings.captionSettings.style; |
|
||||||
style.color = color; |
|
||||||
const newSettings = oldSettings; |
|
||||||
return newSettings; |
|
||||||
}); |
|
||||||
}, |
|
||||||
setCaptionFontSize(size) { |
|
||||||
setSettings((oldSettings) => { |
|
||||||
const style = oldSettings.captionSettings.style; |
|
||||||
style.fontSize = enforceRange(10, size, 60); |
|
||||||
const newSettings = oldSettings; |
|
||||||
return newSettings; |
|
||||||
}); |
|
||||||
}, |
|
||||||
setCaptionBackgroundColor(backgroundColor) { |
|
||||||
setSettings((oldSettings) => { |
|
||||||
const style = oldSettings.captionSettings.style; |
|
||||||
style.backgroundColor = `${style.backgroundColor.substring( |
|
||||||
0, |
|
||||||
7 |
|
||||||
)}${backgroundColor.toString(16).padStart(2, "0")}`;
|
|
||||||
const newSettings = oldSettings; |
|
||||||
return newSettings; |
|
||||||
}); |
|
||||||
}, |
|
||||||
}; |
|
||||||
return settingsContext; |
|
||||||
}, [settings, setSettings]); |
|
||||||
return ( |
|
||||||
<SettingsContext.Provider value={context}> |
|
||||||
{props.children} |
|
||||||
</SettingsContext.Provider> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
export function useSettings() { |
|
||||||
return useContext(SettingsContext); |
|
||||||
} |
|
||||||
|
|
||||||
export default SettingsContext; |
|
@ -1,49 +0,0 @@ |
|||||||
import { createVersionedStore } from "@/utils/storage"; |
|
||||||
|
|
||||||
import { MWSettingsData, MWSettingsDataV1 } from "./types"; |
|
||||||
|
|
||||||
export const SettingsStore = createVersionedStore<MWSettingsData>() |
|
||||||
.setKey("mw-settings") |
|
||||||
.addVersion({ |
|
||||||
version: 0, |
|
||||||
create(): MWSettingsDataV1 { |
|
||||||
return { |
|
||||||
language: "en", |
|
||||||
captionSettings: { |
|
||||||
delay: 0, |
|
||||||
style: { |
|
||||||
color: "#ffffff", |
|
||||||
fontSize: 25, |
|
||||||
backgroundColor: "#00000096", |
|
||||||
}, |
|
||||||
}, |
|
||||||
}; |
|
||||||
}, |
|
||||||
migrate(data: MWSettingsDataV1): MWSettingsData { |
|
||||||
return { |
|
||||||
language: data.language, |
|
||||||
captionSettings: { |
|
||||||
language: "none", |
|
||||||
...data.captionSettings, |
|
||||||
}, |
|
||||||
}; |
|
||||||
}, |
|
||||||
}) |
|
||||||
.addVersion({ |
|
||||||
version: 1, |
|
||||||
create(): MWSettingsData { |
|
||||||
return { |
|
||||||
language: "en", |
|
||||||
captionSettings: { |
|
||||||
delay: 0, |
|
||||||
language: "none", |
|
||||||
style: { |
|
||||||
color: "#ffffff", |
|
||||||
fontSize: 25, |
|
||||||
backgroundColor: "#00000096", |
|
||||||
}, |
|
||||||
}, |
|
||||||
}; |
|
||||||
}, |
|
||||||
}) |
|
||||||
.build(); |
|
@ -1,204 +0,0 @@ |
|||||||
import { |
|
||||||
ReactNode, |
|
||||||
createContext, |
|
||||||
useCallback, |
|
||||||
useContext, |
|
||||||
useMemo, |
|
||||||
useRef, |
|
||||||
} from "react"; |
|
||||||
|
|
||||||
import { DetailedMeta } from "@/backend/metadata/getmeta"; |
|
||||||
import { MWMediaType } from "@/backend/metadata/types/mw"; |
|
||||||
import { useStore } from "@/utils/storage"; |
|
||||||
|
|
||||||
import { VideoProgressStore } from "./store"; |
|
||||||
import { StoreMediaItem, WatchedStoreData, WatchedStoreItem } from "./types"; |
|
||||||
|
|
||||||
const FIVETEEN_MINUTES = 15 * 60; |
|
||||||
const FIVE_MINUTES = 5 * 60; |
|
||||||
|
|
||||||
function shouldSave( |
|
||||||
time: number, |
|
||||||
duration: number, |
|
||||||
isSeries: boolean |
|
||||||
): boolean { |
|
||||||
const timeFromEnd = Math.max(0, duration - time); |
|
||||||
|
|
||||||
// short movie
|
|
||||||
if (duration < FIVETEEN_MINUTES) { |
|
||||||
if (time < 5) return false; |
|
||||||
if (timeFromEnd < 60) return false; |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// long movie
|
|
||||||
if (time < 30) return false; |
|
||||||
if (timeFromEnd < FIVE_MINUTES && !isSeries) return false; |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
interface WatchedStoreDataWrapper { |
|
||||||
updateProgress(media: StoreMediaItem, progress: number, total: number): void; |
|
||||||
getFilteredWatched(): WatchedStoreItem[]; |
|
||||||
removeProgress(id: string): void; |
|
||||||
watched: WatchedStoreData; |
|
||||||
} |
|
||||||
|
|
||||||
const WatchedContext = createContext<WatchedStoreDataWrapper>({ |
|
||||||
updateProgress: () => {}, |
|
||||||
getFilteredWatched: () => [], |
|
||||||
removeProgress: () => {}, |
|
||||||
watched: { |
|
||||||
items: [], |
|
||||||
}, |
|
||||||
}); |
|
||||||
WatchedContext.displayName = "WatchedContext"; |
|
||||||
|
|
||||||
function isSameEpisode(media: StoreMediaItem, v: StoreMediaItem) { |
|
||||||
return ( |
|
||||||
media.meta.id === v.meta.id && |
|
||||||
(!media.series || |
|
||||||
(media.series.seasonId === v.series?.seasonId && |
|
||||||
media.series.episodeId === v.series?.episodeId)) |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
export function WatchedContextProvider(props: { children: ReactNode }) { |
|
||||||
const [watched, setWatched] = useStore(VideoProgressStore); |
|
||||||
|
|
||||||
const contextValue = useMemo( |
|
||||||
() => ({ |
|
||||||
removeProgress(id: string) { |
|
||||||
setWatched((data: WatchedStoreData) => { |
|
||||||
const newData = { ...data }; |
|
||||||
newData.items = newData.items.filter((v) => v.item.meta.id !== id); |
|
||||||
return newData; |
|
||||||
}); |
|
||||||
}, |
|
||||||
updateProgress( |
|
||||||
media: StoreMediaItem, |
|
||||||
progress: number, |
|
||||||
total: number |
|
||||||
): void { |
|
||||||
setWatched((data: WatchedStoreData) => { |
|
||||||
const newData = { ...data }; |
|
||||||
let item = newData.items.find((v) => isSameEpisode(media, v.item)); |
|
||||||
if (!item) { |
|
||||||
item = { |
|
||||||
item: { |
|
||||||
...media, |
|
||||||
meta: { ...media.meta }, |
|
||||||
series: media.series ? { ...media.series } : undefined, |
|
||||||
}, |
|
||||||
progress: 0, |
|
||||||
percentage: 0, |
|
||||||
watchedAt: Date.now(), |
|
||||||
}; |
|
||||||
newData.items.push(item); |
|
||||||
} |
|
||||||
// update actual item
|
|
||||||
item.progress = progress; |
|
||||||
item.percentage = Math.round((progress / total) * 100); |
|
||||||
item.watchedAt = Date.now(); |
|
||||||
|
|
||||||
// remove item if shouldnt save
|
|
||||||
if (!shouldSave(progress, total, !!media.series)) { |
|
||||||
newData.items = data.items.filter( |
|
||||||
(v) => !isSameEpisode(v.item, media) |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
return newData; |
|
||||||
}); |
|
||||||
}, |
|
||||||
getFilteredWatched() { |
|
||||||
let filtered = watched.items; |
|
||||||
|
|
||||||
// get most recently watched for every single item
|
|
||||||
const alreadyFoundMedia: string[] = []; |
|
||||||
filtered = filtered |
|
||||||
.sort((a, b) => { |
|
||||||
return b.watchedAt - a.watchedAt; |
|
||||||
}) |
|
||||||
.filter((item) => { |
|
||||||
const mediaId = item.item.meta.id; |
|
||||||
if (alreadyFoundMedia.includes(mediaId)) return false; |
|
||||||
alreadyFoundMedia.push(mediaId); |
|
||||||
return true; |
|
||||||
}); |
|
||||||
return filtered; |
|
||||||
}, |
|
||||||
watched, |
|
||||||
}), |
|
||||||
[watched, setWatched] |
|
||||||
); |
|
||||||
|
|
||||||
return ( |
|
||||||
<WatchedContext.Provider value={contextValue as any}> |
|
||||||
{props.children} |
|
||||||
</WatchedContext.Provider> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
export function useWatchedContext() { |
|
||||||
return useContext(WatchedContext); |
|
||||||
} |
|
||||||
|
|
||||||
function isSameEpisodeMeta( |
|
||||||
media: StoreMediaItem, |
|
||||||
mediaTwo: DetailedMeta | null, |
|
||||||
episodeId?: string |
|
||||||
) { |
|
||||||
if (mediaTwo?.meta.type === MWMediaType.SERIES && episodeId) { |
|
||||||
return isSameEpisode(media, { |
|
||||||
meta: mediaTwo.meta, |
|
||||||
series: { |
|
||||||
season: 0, |
|
||||||
episode: 0, |
|
||||||
episodeId, |
|
||||||
seasonId: mediaTwo.meta.seasonData.id, |
|
||||||
}, |
|
||||||
}); |
|
||||||
} |
|
||||||
if (!mediaTwo) return () => false; |
|
||||||
return isSameEpisode(media, { meta: mediaTwo.meta }); |
|
||||||
} |
|
||||||
|
|
||||||
export function useWatchedItem(meta: DetailedMeta | null, episodeId?: string) { |
|
||||||
const { watched, updateProgress } = useContext(WatchedContext); |
|
||||||
const item = useMemo( |
|
||||||
() => watched.items.find((v) => isSameEpisodeMeta(v.item, meta, episodeId)), |
|
||||||
[watched, meta, episodeId] |
|
||||||
); |
|
||||||
const lastCommitedTime = useRef([0, 0]); |
|
||||||
|
|
||||||
const callback = useCallback( |
|
||||||
(progress: number, total: number) => { |
|
||||||
const hasChanged = |
|
||||||
lastCommitedTime.current[0] !== progress || |
|
||||||
lastCommitedTime.current[1] !== total; |
|
||||||
if (meta && hasChanged) { |
|
||||||
lastCommitedTime.current = [progress, total]; |
|
||||||
const obj = { |
|
||||||
meta: meta.meta, |
|
||||||
series: |
|
||||||
meta.meta.type === MWMediaType.SERIES && episodeId |
|
||||||
? { |
|
||||||
seasonId: meta.meta.seasonData.id, |
|
||||||
episodeId, |
|
||||||
season: meta.meta.seasonData.number, |
|
||||||
episode: |
|
||||||
meta.meta.seasonData.episodes.find( |
|
||||||
(ep) => ep.id === episodeId |
|
||||||
)?.number || 0, |
|
||||||
} |
|
||||||
: undefined, |
|
||||||
}; |
|
||||||
updateProgress(obj, progress, total); |
|
||||||
} |
|
||||||
}, |
|
||||||
[meta, updateProgress, episodeId] |
|
||||||
); |
|
||||||
|
|
||||||
return { updateProgress: callback, watchedItem: item }; |
|
||||||
} |
|
@ -1,8 +1,8 @@ |
|||||||
import { MWMediaType } from "@/backend/metadata/types/mw"; |
import { MWMediaType } from "@/backend/metadata/types/mw"; |
||||||
import { BookmarkMediaItem, useBookmarkStore } from "@/stores/bookmarks"; |
import { BookmarkMediaItem, useBookmarkStore } from "@/stores/bookmarks"; |
||||||
import { createVersionedStore } from "@/utils/storage"; |
|
||||||
|
|
||||||
import { BookmarkStoreData } from "./types"; |
import { BookmarkStoreData } from "./types"; |
||||||
|
import { createVersionedStore } from "../migrations"; |
||||||
import { OldBookmarks, migrateV1Bookmarks } from "../watched/migrations/v2"; |
import { OldBookmarks, migrateV1Bookmarks } from "../watched/migrations/v2"; |
||||||
import { migrateV2Bookmarks } from "../watched/migrations/v3"; |
import { migrateV2Bookmarks } from "../watched/migrations/v3"; |
||||||
|
|
@ -0,0 +1,68 @@ |
|||||||
|
import { useLanguageStore } from "@/stores/language"; |
||||||
|
import { useSubtitleStore } from "@/stores/subtitles"; |
||||||
|
|
||||||
|
import { MWSettingsData, MWSettingsDataV1 } from "./types"; |
||||||
|
import { createVersionedStore } from "../migrations"; |
||||||
|
|
||||||
|
export const SettingsStore = createVersionedStore<Record<never, never>>() |
||||||
|
.setKey("mw-settings") |
||||||
|
.addVersion({ |
||||||
|
version: 0, |
||||||
|
create(): MWSettingsDataV1 { |
||||||
|
return { |
||||||
|
language: "en", |
||||||
|
captionSettings: { |
||||||
|
delay: 0, |
||||||
|
style: { |
||||||
|
color: "#ffffff", |
||||||
|
fontSize: 25, |
||||||
|
backgroundColor: "#00000096", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
}, |
||||||
|
migrate(data: MWSettingsDataV1): MWSettingsData { |
||||||
|
return { |
||||||
|
language: data.language, |
||||||
|
captionSettings: { |
||||||
|
language: "none", |
||||||
|
...data.captionSettings, |
||||||
|
}, |
||||||
|
}; |
||||||
|
}, |
||||||
|
}) |
||||||
|
.addVersion({ |
||||||
|
version: 1, |
||||||
|
migrate(old: MWSettingsData): Record<never, never> { |
||||||
|
const langStore = useLanguageStore.getState(); |
||||||
|
const subtitleStore = useSubtitleStore.getState(); |
||||||
|
|
||||||
|
const backgroundColor = old.captionSettings.style.backgroundColor; |
||||||
|
let backgroundOpacity = 0.5; |
||||||
|
if (backgroundColor.length === 9) { |
||||||
|
const opacitySplit = backgroundColor.slice(7); // '#' + 6 digits
|
||||||
|
backgroundOpacity = parseInt(opacitySplit, 16) / 255; // read as hex;
|
||||||
|
} |
||||||
|
|
||||||
|
langStore.setLanguage(old.language); |
||||||
|
subtitleStore.updateStyling({ |
||||||
|
backgroundOpacity, |
||||||
|
color: old.captionSettings.style.color, |
||||||
|
size: old.captionSettings.style.fontSize / 25, |
||||||
|
}); |
||||||
|
subtitleStore.importSubtitleLanguage( |
||||||
|
old.captionSettings.language === "none" |
||||||
|
? null |
||||||
|
: old.captionSettings.language |
||||||
|
); |
||||||
|
|
||||||
|
return {}; |
||||||
|
}, |
||||||
|
}) |
||||||
|
.addVersion({ |
||||||
|
version: 2, |
||||||
|
create(): Record<never, never> { |
||||||
|
return {}; |
||||||
|
}, |
||||||
|
}) |
||||||
|
.build(); |
@ -0,0 +1,29 @@ |
|||||||
|
import { useVolumeStore } from "@/stores/volume"; |
||||||
|
|
||||||
|
import { createVersionedStore } from "../migrations"; |
||||||
|
|
||||||
|
interface VolumeStoreData { |
||||||
|
volume: number; |
||||||
|
} |
||||||
|
|
||||||
|
export const volumeStore = createVersionedStore<Record<never, never>>() |
||||||
|
.setKey("mw-volume") |
||||||
|
.addVersion({ |
||||||
|
version: 0, |
||||||
|
create() { |
||||||
|
return { |
||||||
|
volume: 1, |
||||||
|
}; |
||||||
|
}, |
||||||
|
migrate(data: VolumeStoreData): Record<never, never> { |
||||||
|
useVolumeStore.getState().setVolume(data.volume); |
||||||
|
return {}; |
||||||
|
}, |
||||||
|
}) |
||||||
|
.addVersion({ |
||||||
|
version: 1, |
||||||
|
create() { |
||||||
|
return {}; |
||||||
|
}, |
||||||
|
}) |
||||||
|
.build(); |
@ -1,10 +1,10 @@ |
|||||||
import { useProgressStore } from "@/stores/progress"; |
import { useProgressStore } from "@/stores/progress"; |
||||||
import { createVersionedStore } from "@/utils/storage"; |
|
||||||
|
|
||||||
import { OldData, migrateV2Videos } from "./migrations/v2"; |
import { OldData, migrateV2Videos } from "./migrations/v2"; |
||||||
import { migrateV3Videos } from "./migrations/v3"; |
import { migrateV3Videos } from "./migrations/v3"; |
||||||
import { migrateV4Videos } from "./migrations/v4"; |
import { migrateV4Videos } from "./migrations/v4"; |
||||||
import { WatchedStoreData } from "./types"; |
import { WatchedStoreData } from "./types"; |
||||||
|
import { createVersionedStore } from "../migrations"; |
||||||
|
|
||||||
export const VideoProgressStore = createVersionedStore<WatchedStoreData>() |
export const VideoProgressStore = createVersionedStore<WatchedStoreData>() |
||||||
.setKey("video-progress") |
.setKey("video-progress") |
@ -0,0 +1,29 @@ |
|||||||
|
import { useEffect } from "react"; |
||||||
|
import { create } from "zustand"; |
||||||
|
import { immer } from "zustand/middleware/immer"; |
||||||
|
|
||||||
|
import i18n from "@/setup/i18n"; |
||||||
|
|
||||||
|
export interface LanguageStore { |
||||||
|
language: string; |
||||||
|
setLanguage(v: string): void; |
||||||
|
} |
||||||
|
|
||||||
|
export const useLanguageStore = create( |
||||||
|
immer<LanguageStore>((set) => ({ |
||||||
|
language: "en", |
||||||
|
setLanguage(v) { |
||||||
|
set((s) => { |
||||||
|
s.language = v; |
||||||
|
}); |
||||||
|
}, |
||||||
|
})) |
||||||
|
); |
||||||
|
|
||||||
|
export function useLanguageListener() { |
||||||
|
const language = useLanguageStore((s) => s.language); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
i18n.changeLanguage(language); |
||||||
|
}, [language]); |
||||||
|
} |
Loading…
Reference in new issue