Files
stash/ui/v2.5/src/hooks/Interactive/context.tsx
xtc1337 15bf28d5be Adding the ability to support different Haptic Devices (#5856)
* refactored `Interactive` class to allow more HapticDevice devices
* simplified api hooks
* update creation of `interactive` to pass `stashConfig`
* updated UIPluginApi to mention `PluginApi.InteractiveUtils`
2025-09-25 15:27:58 +10:00

253 lines
6.5 KiB
TypeScript

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<void>;
uploadScript: (funscriptPath: string) => Promise<void>;
sync: () => Promise<void>;
}
export const InteractiveContext = React.createContext<IState>({
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<IInteractiveState>(
LOCAL_FORAGE_KEY,
{ serverOffset: 0, lastSyncTime: 0 }
);
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
const [state, setState] = useState<ConnectionState>(ConnectionState.Missing);
const [handyKey, setHandyKey] = useState<string | undefined>(undefined);
const [currentScript, setCurrentScript] = useState<string | undefined>(
undefined
);
const [scriptOffset, setScriptOffset] = useState<number>(0);
const [useStashHostedFunscript, setUseStashHostedFunscript] =
useState<boolean>(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<string | undefined>();
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 (
<InteractiveContext.Provider
value={{
interactive,
state,
error,
currentScript,
serverOffset: config?.serverOffset ?? 0,
initialised,
initialise,
uploadScript,
sync,
}}
>
{children}
</InteractiveContext.Provider>
);
};
export const useInteractive = () => {
return useContext(InteractiveContext);
};
export default InteractiveProvider;