import React, { useState, useContext, useCallback } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { Alert, Button, Card, Container, Form, InputGroup, } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; import { mutateSetup, useConfigureUI, useSystemStatus, } from "src/core/StashService"; import { useHistory } from "react-router-dom"; import { ConfigurationContext } from "src/hooks/Config"; import StashConfiguration from "../Settings/StashConfiguration"; import { Icon } from "../Shared/Icon"; import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { ModalComponent } from "../Shared/Modal"; import { FolderSelectDialog } from "../Shared/FolderSelect/FolderSelectDialog"; import { faEllipsisH, faExclamationTriangle, faQuestionCircle, } from "@fortawesome/free-solid-svg-icons"; import { releaseNotes } from "src/docs/en/ReleaseNotes"; import { ExternalLink } from "../Shared/ExternalLink"; interface ISetupContextState { configuration: GQL.ConfigDataFragment; systemStatus: GQL.SystemStatusQuery; setupState: Partial; setupError: string | undefined; pathJoin: (...paths: string[]) => string; pathDir(path: string): string; homeDir: string; windows: boolean; macApp: boolean; homeDirPath: string; pwd: string; workingDir: string; } const SetupStateContext = React.createContext(null); const useSetupContext = () => { const context = React.useContext(SetupStateContext); if (context === null) { throw new Error("useSettings must be used within a SettingsContext"); } return context; }; const SetupContext: React.FC<{ setupState: Partial; setupError: string | undefined; systemStatus: GQL.SystemStatusQuery; configuration: GQL.ConfigDataFragment; }> = ({ setupState, setupError, systemStatus, configuration, children }) => { const status = systemStatus?.systemStatus; const windows = status?.os === "windows"; const pathSep = windows ? "\\" : "/"; const homeDir = windows ? "%USERPROFILE%" : "$HOME"; const pwd = windows ? "%CD%" : "$PWD"; const pathJoin = useCallback( (...paths: string[]) => { return paths.join(pathSep); }, [pathSep] ); // simply returns everything preceding the last path separator function pathDir(path: string) { const lastSep = path.lastIndexOf(pathSep); if (lastSep === -1) return ""; return path.slice(0, lastSep); } const workingDir = status?.workingDir ?? "."; // When running Stash.app, the working directory is (usually) set to /. // Assume that the user doesn't want to set up in / (it's usually mounted read-only anyway), // so in this situation disallow setting up in the working directory. const macApp = status?.os === "darwin" && workingDir === "/"; const homeDirPath = pathJoin(status?.homeDir ?? homeDir, ".stash"); const state: ISetupContextState = { systemStatus, configuration, windows, macApp, pathJoin, pathDir, homeDir, homeDirPath, pwd, workingDir, setupState, setupError, }; return ( {children} ); }; interface IWizardStep { next: (input?: Partial) => void; goBack: () => void; } const WelcomeSpecificConfig: React.FC = ({ next }) => { const { systemStatus } = useSetupContext(); const status = systemStatus?.systemStatus; const overrideConfig = status?.configPath; function onNext() { next({ configLocation: overrideConfig! }); } return ( <>

{chunks}, }} />

); }; const DefaultWelcomeStep: React.FC = ({ next }) => { const { pathJoin, homeDir, macApp, homeDirPath, pwd, workingDir } = useSetupContext(); const fallbackStashDir = pathJoin(homeDir, ".stash"); const fallbackConfigPath = pathJoin(fallbackStashDir, "config.yml"); function onConfigLocationChosen(inWorkingDir: boolean) { const configLocation = inWorkingDir ? "config.yml" : ""; next({ configLocation }); } return ( <>

{chunks}, fallback_path: fallbackConfigPath, }} />

{chunks}, }} />

); }; const WelcomeStep: React.FC = (props) => { const { systemStatus } = useSetupContext(); const status = systemStatus?.systemStatus; const overrideConfig = status?.configPath; return overrideConfig ? ( ) : ( ); }; const StashAlert: React.FC<{ close: (confirm: boolean) => void }> = ({ close, }) => { const intl = useIntl(); return ( close(true), }} cancel={{ onClick: () => close(false) }} >

); }; const DatabaseSection: React.FC<{ databaseFile: string; setDatabaseFile: React.Dispatch>; }> = ({ databaseFile, setDatabaseFile }) => { const intl = useIntl(); return (

{chunks}, }} />
{chunks}, }} />

setDatabaseFile(e.currentTarget.value)} />
); }; const DirectorySelector: React.FC<{ value: string; setValue: React.Dispatch>; placeholder: string; disabled?: boolean; }> = ({ value, setValue, placeholder, disabled = false }) => { const [showSelectDialog, setShowSelectDialog] = useState(false); function onSelectClosed(dir?: string) { if (dir) { setValue(dir); } setShowSelectDialog(false); } return ( <> {showSelectDialog ? ( ) : null} setValue(e.currentTarget.value)} disabled={disabled} /> ); }; const GeneratedSection: React.FC<{ generatedLocation: string; setGeneratedLocation: React.Dispatch>; }> = ({ generatedLocation, setGeneratedLocation }) => { const intl = useIntl(); return (

{chunks}, }} />

); }; const CacheSection: React.FC<{ cacheLocation: string; setCacheLocation: React.Dispatch>; }> = ({ cacheLocation, setCacheLocation }) => { const intl = useIntl(); return (

{chunks}, }} />

); }; const BlobsSection: React.FC<{ blobsLocation: string; setBlobsLocation: React.Dispatch>; storeBlobsInDatabase: boolean; setStoreBlobsInDatabase: React.Dispatch>; }> = ({ blobsLocation, setBlobsLocation, storeBlobsInDatabase, setStoreBlobsInDatabase, }) => { const intl = useIntl(); return (

{chunks}, }} />

{chunks}, strong: (chunks: string) => {chunks}, }} />

setStoreBlobsInDatabase(!storeBlobsInDatabase)} />
); }; const SetPathsStep: React.FC = ({ goBack, next }) => { const { configuration } = useSetupContext(); const [showStashAlert, setShowStashAlert] = useState(false); const [stashes, setStashes] = useState([]); const [databaseFile, setDatabaseFile] = useState(""); const [generatedLocation, setGeneratedLocation] = useState(""); const [cacheLocation, setCacheLocation] = useState(""); const [storeBlobsInDatabase, setStoreBlobsInDatabase] = useState(false); const [blobsLocation, setBlobsLocation] = useState(""); const overrideDatabase = configuration?.general.databasePath; const overrideGenerated = configuration?.general.generatedPath; const overrideCache = configuration?.general.cachePath; const overrideBlobs = configuration?.general.blobsPath; function preNext() { if (stashes.length === 0) { setShowStashAlert(true); } else { onNext(); } } function onNext() { const input: Partial = { stashes, databaseFile, generatedLocation, cacheLocation, blobsLocation: storeBlobsInDatabase ? "" : blobsLocation, storeBlobsInDatabase, }; next(input); } return ( <> {showStashAlert ? ( { setShowStashAlert(false); if (confirm) { onNext(); } }} /> ) : null}

setStashes(s)} />
{overrideDatabase ? null : ( )} {overrideGenerated ? null : ( )} {overrideCache ? null : ( )} {overrideBlobs ? null : ( )}
); }; const StashExclusions: React.FC<{ stash: GQL.StashConfig }> = ({ stash }) => { if (!stash.excludeImage && !stash.excludeVideo) { return null; } const excludes = []; if (stash.excludeVideo) { excludes.push("videos"); } if (stash.excludeImage) { excludes.push("images"); } return {`(excludes ${excludes.join(" and ")})`}; }; const ConfirmStep: React.FC = ({ goBack, next }) => { const { configuration, pathDir, pathJoin, pwd, setupState } = useSetupContext(); const cfgFile = setupState.configLocation ? setupState.configLocation : pathJoin(pwd, "config.yml"); const cfgDir = pathDir(cfgFile); const stashes = setupState.stashes ?? []; const { databaseFile, generatedLocation, cacheLocation, blobsLocation, storeBlobsInDatabase, } = setupState; const overrideDatabase = configuration?.general.databasePath; const overrideGenerated = configuration?.general.generatedPath; const overrideCache = configuration?.general.cachePath; const overrideBlobs = configuration?.general.blobsPath; function joinCfgDir(path: string) { if (cfgDir) { return pathJoin(cfgDir, path); } else { return path; } } return ( <>

{cfgFile}
    {stashes.map((s) => (
  • {s.path}
  • ))}
{!overrideDatabase && (
{databaseFile || joinCfgDir("stash-go.sqlite")}
)} {!overrideGenerated && (
{generatedLocation || joinCfgDir("generated")}
)} {!overrideCache && (
{cacheLocation || joinCfgDir("cache")}
)} {!overrideBlobs && (
{storeBlobsInDatabase ? ( ) : ( blobsLocation || joinCfgDir("blobs") )}
)}
); }; const DiscordLink = ( Discord ); const GithubLink = ( ); const ErrorStep: React.FC<{ error: string; goBack: () => void }> = ({ error, goBack, }) => { return ( <>

{error} }} />

); }; const SuccessStep: React.FC<{}> = () => { const intl = useIntl(); const history = useHistory(); const [mutateDownloadFFMpeg] = GQL.useDownloadFfMpegMutation(); const [downloadFFmpeg, setDownloadFFmpeg] = useState(true); const { systemStatus } = useSetupContext(); const status = systemStatus?.systemStatus; function onFinishClick() { if ((!status?.ffmpegPath || !status?.ffprobePath) && downloadFFmpeg) { mutateDownloadFFMpeg(); } history.push("/settings?tab=library"); } return ( <>

{chunks}, localized_task: intl.formatMessage({ id: "config.categories.tasks", }), localized_scan: intl.formatMessage({ id: "actions.scan" }), }} />

{!status?.ffmpegPath || !status?.ffprobePath ? ( <> {chunks}, }} />

setDownloadFFmpeg(!downloadFFmpeg)} />

) : null}

}} />

Open Collective ), }} />

); }; const FinishStep: React.FC = ({ goBack }) => { const { setupError } = useSetupContext(); if (setupError !== undefined) { return ; } return ; }; export const Setup: React.FC = () => { const intl = useIntl(); const { configuration, loading: configLoading } = useContext(ConfigurationContext); const [saveUI] = useConfigureUI(); const { data: systemStatus, loading: statusLoading, error: statusError, } = useSystemStatus(); const [step, setStep] = useState(0); const [setupInput, setSetupInput] = useState>({}); const [creating, setCreating] = useState(false); const [setupError, setSetupError] = useState(undefined); const history = useHistory(); const steps: React.FC[] = [ WelcomeStep, SetPathsStep, ConfirmStep, FinishStep, ]; const Step = steps[step]; async function createSystem() { try { setCreating(true); setSetupError(undefined); await mutateSetup(setupInput as GQL.SetupInput); // Set lastNoteSeen to hide release notes dialog await saveUI({ variables: { input: { ...configuration?.ui, lastNoteSeen: releaseNotes[0].date, }, }, }); } catch (e) { if (e instanceof Error && e.message) { setSetupError(e.message); } else { setSetupError(String(e)); } } finally { setCreating(false); setStep(step + 1); } } function next(input?: Partial) { setSetupInput({ ...setupInput, ...input }); if (Step === ConfirmStep) { // create the system createSystem(); } else { setStep(step + 1); } } function goBack() { if (Step === FinishStep) { // go back to the step before ConfirmStep setStep(step - 2); } else { setStep(step - 1); } } if (configLoading || statusLoading) { return ; } if ( step === 0 && systemStatus && systemStatus.systemStatus.status !== GQL.SystemStatusEnum.Setup ) { // redirect to main page history.push("/"); return ; } if (statusError) { return ( ); } if (!configuration || !systemStatus) { return ( ); } return (

{creating ? ( ) : ( )}
); }; export default Setup;