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 (
<>
>
);
};
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 (
<>
>
);
};
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}
>
);
};
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;