import React, { useCallback, useContext, useEffect, useState } from "react"; import { ConfigurationContext } from "../Config"; import { useLocalForage } from "../LocalForage"; import { Interactive as InteractiveAPI } from "./interactive"; import InteractiveUtils, { IInteractiveClient, IInteractiveClientProvider, } from "./utils"; export enum ConnectionState { Missing, Disconnected, Error, Connecting, Syncing, Uploading, Ready, } export function connectionStateLabel(s: ConnectionState) { const prefix = "handy_connection_status"; switch (s) { case ConnectionState.Missing: return `${prefix}.missing`; case ConnectionState.Connecting: return `${prefix}.connecting`; case ConnectionState.Disconnected: return `${prefix}.disconnected`; case ConnectionState.Error: return `${prefix}.error`; case ConnectionState.Syncing: return `${prefix}.syncing`; case ConnectionState.Uploading: return `${prefix}.uploading`; case ConnectionState.Ready: return `${prefix}.ready`; } } export interface IState { interactive: IInteractiveClient; state: ConnectionState; serverOffset: number; initialised: boolean; currentScript?: string; error?: string; initialise: () => Promise; uploadScript: (funscriptPath: string) => Promise; sync: () => Promise; } export const InteractiveContext = React.createContext({ interactive: new InteractiveAPI("", 0), state: ConnectionState.Missing, serverOffset: 0, initialised: false, initialise: () => { return Promise.resolve(); }, uploadScript: () => { return Promise.resolve(); }, sync: () => { return Promise.resolve(); }, }); const LOCAL_FORAGE_KEY = "interactive"; const TIME_BETWEEN_SYNCS = 60 * 60 * 1000; // 1 hour interface IInteractiveState { serverOffset: number; lastSyncTime: number; } export const defaultInteractiveClientProvider: IInteractiveClientProvider = ({ handyKey, scriptOffset, }): IInteractiveClient => { return new InteractiveAPI(handyKey, scriptOffset); }; export const InteractiveProvider: React.FC = ({ children }) => { const [{ data: config }, setConfig] = useLocalForage( LOCAL_FORAGE_KEY, { serverOffset: 0, lastSyncTime: 0 } ); const { configuration: stashConfig } = React.useContext(ConfigurationContext); const [state, setState] = useState(ConnectionState.Missing); const [handyKey, setHandyKey] = useState(undefined); const [currentScript, setCurrentScript] = useState( undefined ); const [scriptOffset, setScriptOffset] = useState(0); const [useStashHostedFunscript, setUseStashHostedFunscript] = useState(false); const resolveInteractiveClient = useCallback(() => { const interactiveClientProvider = InteractiveUtils.interactiveClientProvider ?? defaultInteractiveClientProvider; return interactiveClientProvider({ handyKey: "", scriptOffset: 0, defaultClientProvider: defaultInteractiveClientProvider, stashConfig, }); }, [stashConfig]); // fetch client provider from PluginApi if not found use default provider const [interactive] = useState(resolveInteractiveClient); const [initialised, setInitialised] = useState(false); const [error, setError] = useState(); const initialise = useCallback(async () => { setError(undefined); const shouldResync = !config?.lastSyncTime || Date.now() - config?.lastSyncTime > TIME_BETWEEN_SYNCS; if (!config?.serverOffset || shouldResync) { setState(ConnectionState.Syncing); const offset = await interactive.sync(); setConfig({ serverOffset: offset, lastSyncTime: Date.now() }); } if (config?.serverOffset) { await interactive.configure({ estimatedServerTimeOffset: config.serverOffset, }); setState(ConnectionState.Connecting); try { await interactive.connect(); setState(ConnectionState.Ready); setInitialised(true); } catch (e) { if (e instanceof Error) { setError(e.message ?? e.toString()); setState(ConnectionState.Error); } } } }, [config, interactive, setConfig]); useEffect(() => { if (!stashConfig) { return; } setHandyKey(stashConfig.interface.handyKey ?? undefined); setScriptOffset(stashConfig.interface.funscriptOffset ?? 0); setUseStashHostedFunscript( stashConfig.interface.useStashHostedFunscript ?? false ); }, [stashConfig]); useEffect(() => { if (!config) { return; } const oldKey = interactive.handyKey; interactive .configure({ connectionKey: handyKey ?? "", offset: scriptOffset, useStashHostedFunscript, }) .then(() => { if (oldKey !== interactive.handyKey && interactive.handyKey) { initialise(); } }); }, [ handyKey, scriptOffset, useStashHostedFunscript, config, interactive, initialise, ]); const sync = useCallback(async () => { if ( !interactive.handyKey || state === ConnectionState.Syncing || !initialised ) { return; } setState(ConnectionState.Syncing); const offset = await interactive.sync(); setConfig({ serverOffset: offset, lastSyncTime: Date.now() }); setState(ConnectionState.Ready); }, [interactive, state, setConfig, initialised]); const uploadScript = useCallback( async (funscriptPath: string) => { await interactive.pause(); if ( !interactive.handyKey || !funscriptPath || funscriptPath === currentScript ) { return Promise.resolve(); } setState(ConnectionState.Uploading); try { await interactive.uploadScript( funscriptPath, stashConfig?.general?.apiKey ); setCurrentScript(funscriptPath); setState(ConnectionState.Ready); } catch (e) { setState(ConnectionState.Error); } }, [interactive, currentScript, stashConfig] ); return ( {children} ); }; export const useInteractive = () => { return useContext(InteractiveContext); }; export default InteractiveProvider;