import { activeWindow } from "electron-util" ; import { NotificationFunctionOptions , NotificationTypes } from "vue-notifications" ; import { ActionContext } from "vuex" ; import { getStoreAccessors } from "vuex-typescript" ; import { addBreadcrumb , Severity } from "@sentry/browser" ; import { EpisodeListEpisodes , ListEntry as IListEntry , MediaListStatus , Provider , } from "@/graphql/generated/types" ; import { router } from "@/router" ; import { RootState } from "@/state/store" ; import { generateId , isNil , pluck , propEq } from "@/utils" ; export type Toast = { id : string ; title : string ; message : string ; type : NotificationTypes ; click ? : ( ... a : any [ ] ) => any ; } ; type ToastOptions = NotificationFunctionOptions < NotificationTypes > & { click ? : ( ... a : any [ ] ) => any ; } ; type AddToastMutationOptions = NotificationFunctionOptions < NotificationTypes > & { id : string ; click ? : ( ... a : any [ ] ) => any ; } ; export type ListEntry = { id : number ; status : MediaListStatus ; progress : number ; } ; export type Sequel = { id : number ; title : string ; bannerImage : string ; } ; type ModalBase = { visible : boolean ; } ; export type EditModalAnime = { animeId : number ; title : string ; bannerImage : string ; episodes : number | null ; listEntry : Omit < IListEntry , "__typename" | "mediaId" | "media" > ; } ; export type ManualSearchOptions = { provider : Provider ; anilistId : number | null ; selectedEpisodes : EpisodeListEpisodes [ ] ; } ; export type LocalSourceOptions = { anilistId : number ; } ; export type AppState = { isUpdateAvailable : string | null ; toasts : Toast [ ] ; isFullscreen : boolean ; anilistRequests : number ; modals : { about : ModalBase ; edit : ModalBase & { anime : EditModalAnime | null ; } ; manualSearch : ModalBase & ManualSearchOptions ; localSource : ModalBase & { options : LocalSourceOptions | null ; } ; } ; } ; export type ModalName = keyof AppState [ "modals" ] ; type AppContext = ActionContext < AppState , RootState > ; const initialModalBase = { visible : false , } ; const initialState = { isUpdateAvailable : null , toasts : [ ] , isFullscreen : false , anilistRequests : 85 , modals : { about : { ... initialModalBase } , edit : { ... initialModalBase , anime : null , } , manualSearch : { ... initialModalBase , provider : Provider . Crunchyroll , anilistId : null , selectedEpisodes : [ ] , } , localSource : { ... initialModalBase , options : null , } , } , } ; export const app = { namespaced : true , state : { ... initialState } , getters : { getUpdateUrl ( state ) { return state . isUpdateAvailable ; } , getToasts ( state ) { return state . toasts ; } , getModalStates ( state ) { const entries = Object . entries ( state . modals ) . map ( ( [ key , obj ] ) => [ key , obj . visible ] as [ string , boolean ] ) ; return entries . reduce ( ( obj , [ key , value ] ) => { obj [ key as keyof AppState [ "modals" ] ] = value ; return obj ; } , { } as Record < keyof AppState [ "modals" ] , boolean > ) ; } , getEditingAnime ( state ) { return state . modals . edit . anime ; } , getManualSearchOptions ( state ) { return state . modals . manualSearch ; } , getLocalSourceAnime ( state ) { return state . modals . localSource . options ; } , getIsFullscreen ( state ) { return state . isFullscreen ; } , getSelectedEpisodes ( state ) { return state . modals . manualSearch . selectedEpisodes ; } , getAnilistRequestsUntilLimiting ( state ) { return state . anilistRequests ; } , } , mutations : { setIsUpdateAvailable ( state , url ) { state . isUpdateAvailable = url ; } , addToast ( state , payload ) { state . toasts = [ ... state . toasts , { id : payload . id , title : payload . title , message : payload . message , type : payload . type , click : payload . click , } , ] ; } , removeToast ( state , id ) { state . toasts = state . toasts . filter ( ( toast ) => ! propEq < typeof toast , "id" > ( "id" , id ) ( toast ) ) ; } , toggleModal ( state , modal ) { state . modals [ modal ] . visible = ! state . modals [ modal ] . visible ; } , closeAllModals ( state ) { Object . keys ( state . modals ) . forEach ( ( modal ) => { ; ( state . modals as any ) [ modal ] . visible = false ; } ) ; } , setEditingAnime ( state , anime ) { state . modals . edit . anime = JSON . parse ( JSON . stringify ( anime ) ) ; } , setEditingAnimeValue < K extends keyof EditModalAnime [ "listEntry" ] > ( state , payload ) { if ( ! state . modals . edit . anime ) return ; state . modals . edit . anime . listEntry [ payload . key ] = payload . value ; } , setManualSearchOptions ( state , options ) { state . modals . manualSearch = { ... state . modals . manualSearch , ... options , } ; } , setLocalSourceAnime ( state , animeId ) { state . modals . localSource . visible = ! isNil ( animeId ) ; state . modals . localSource . options = ! animeId ? null : { anilistId : animeId , } ; } , setFullscreen ( state , b ) { const browserWindow = activeWindow ( ) ; state . isFullscreen = b ; browserWindow . setFullScreen ( b ) ; if ( state . isFullscreen ) { router . push ( "/player-full" ) ; } else { router . back ( ) ; } } , selectCrunchyrollEpisodes ( state , episodes ) { const { selectedEpisodes } = state . modals . manualSearch ; const selectedIds = pluck ( "id" , selectedEpisodes ) ; const filteredEpisodes = episodes . filter ( ( { id } ) => ! selectedIds . includes ( id ) ) ; let count = selectedEpisodes . length ; let lastNumber = 0 ; let duplicates = 0 ; const episodeWithCorrectNumbers = filteredEpisodes . map ( ( { episodeNumber , ... rest } ) => { let newNumber = count + 1 ; if ( episodeNumber === lastNumber ) { newNumber = count ; } const newEpisodeNumber = newNumber - duplicates ; const selectedEpisode = { ... rest , index : newEpisodeNumber - 1 , episodeNumber : newEpisodeNumber , } ; if ( episodeNumber === lastNumber ) { duplicates ++ ; } lastNumber = episodeNumber ; count ++ ; return selectedEpisode ; } ) ; state . modals . manualSearch . selectedEpisodes = [ ... state . modals . manualSearch . selectedEpisodes , ... episodeWithCorrectNumbers , ] ; } , unselectCrunchyrollEpisodes ( state , ids ) { const { selectedEpisodes } = state . modals . manualSearch ; const remainingEpisodes = selectedEpisodes . filter ( ( episode ) => ! ids . find ( ( id ) => id === episode . id ) ) ; let lastEpNumber = 0 ; const episodesWithFixedNumbers = remainingEpisodes . map ( ( episode ) => { const realNum = lastEpNumber + 1 ; lastEpNumber = realNum ; return { ... episode , episodeNumber : realNum , } ; } ) ; state . modals . manualSearch . selectedEpisodes = episodesWithFixedNumbers ; } , setAnilistRequests ( state , requests ) { state . anilistRequests = requests ; } , } , actions : { sendToast ( context , options ) { const realOptions = Object . assign ( { } , { timeout : 4000 , } , options ) ; const id = generateId ( ) ; addToast ( context , { id , ... realOptions } ) ; if ( realOptions . consoleMessage && ( console as any ) [ realOptions . type ] ) { ; ( console as any ) [ realOptions . type ] ( realOptions . consoleMessage ) ; } addBreadcrumb ( { category : "toast" , level : realOptions . type !== "success" ? ( realOptions . type as Severity ) : Severity . Info , type : realOptions . type === "error" ? "error" : "default" , message : `${ options . title } - ${ options . message }` , } ) ; setTimeout ( ( ) => { removeToast ( context , id ) ; } , realOptions . timeout ) ; } , sendErrorToast ( context , error ) { sendToast ( context , { type : "error" , title : "An error occurred!" , message : error , } ) ; } , initEditModal ( context , anime ) { setEditingAnime ( context , anime ) ; toggleModal ( context , "edit" ) ; } , initManualSearch ( context , options ) { setManualSearchOptions ( context , { ... options , selectedEpisodes : [ ] , } ) ; toggleModal ( context , "manualSearch" ) ; } , } , } ; const { read , commit , dispatch } = getStoreAccessors < AppState , RootState > ( "app" ) ; export const getUpdateUrl = read ( app . getters . getUpdateUrl ) ; export const getToasts = read ( app . getters . getToasts ) ; export const getModalStates = read ( app . getters . getModalStates ) ; export const getEditingAnime = read ( app . getters . getEditingAnime ) ; export const getManualSearchOptions = read ( app . getters . getManualSearchOptions ) ; export const getLocalSourceOptions = read ( app . getters . getLocalSourceAnime ) ; export const getIsFullscreen = read ( app . getters . getIsFullscreen ) ; export const getSelectedEpisodes = read ( app . getters . getSelectedEpisodes ) ; export const getAnilistRequestsUntilLimiting = read ( app . getters . getAnilistRequestsUntilLimiting ) ; export const setIsUpdateAvailable = commit ( app . mutations . setIsUpdateAvailable ) ; const setEditingAnime = commit ( app . mutations . setEditingAnime ) ; export const setEditingAnimeValue = commit ( app . mutations . setEditingAnimeValue ) ; export const setManualSearchOptions = commit ( app . mutations . setManualSearchOptions ) ; export const setLocalSourceAnime = commit ( app . mutations . setLocalSourceAnime ) ; export const removeToast = commit ( app . mutations . removeToast ) ; export const toggleModal = commit ( app . mutations . toggleModal ) ; const addToast = commit ( app . mutations . addToast ) ; export const closeAllModals = commit ( app . mutations . closeAllModals ) ; export const setFullscreen = commit ( app . mutations . setFullscreen ) ; export const selectCrunchyrollEpisodes = commit ( app . mutations . selectCrunchyrollEpisodes ) ; export const unselectCrunchyrollEpisodes = commit ( app . mutations . unselectCrunchyrollEpisodes ) ; export const setAnilistRequests = commit ( app . mutations . setAnilistRequests ) ; export const sendToast = dispatch ( app . actions . sendToast ) ; export const sendErrorToast = dispatch ( app . actions . sendErrorToast ) ; export const initEditModal = dispatch ( app . actions . initEditModal ) ; export const initManualSearch = dispatch ( app . actions . initManualSearch ) ;