mirror of
https://github.com/stashapp/stash.git
synced 2025-12-07 08:54:10 +01:00
1075 lines
29 KiB
TypeScript
1075 lines
29 KiB
TypeScript
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<GQL.SetupInput>;
|
|
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<ISetupContextState | null>(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<GQL.SetupInput>;
|
|
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 (
|
|
<SetupStateContext.Provider value={state}>
|
|
{children}
|
|
</SetupStateContext.Provider>
|
|
);
|
|
};
|
|
|
|
interface IWizardStep {
|
|
next: (input?: Partial<GQL.SetupInput>) => void;
|
|
goBack: () => void;
|
|
}
|
|
|
|
const WelcomeSpecificConfig: React.FC<IWizardStep> = ({ next }) => {
|
|
const { systemStatus } = useSetupContext();
|
|
const status = systemStatus?.systemStatus;
|
|
const overrideConfig = status?.configPath;
|
|
|
|
function onNext() {
|
|
next({ configLocation: overrideConfig! });
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<section>
|
|
<h2 className="mb-5">
|
|
<FormattedMessage id="setup.welcome_to_stash" />
|
|
</h2>
|
|
<p className="lead text-center">
|
|
<FormattedMessage id="setup.welcome_specific_config.unable_to_locate_specified_config" />
|
|
</p>
|
|
<p>
|
|
<FormattedMessage
|
|
id="setup.welcome_specific_config.config_path"
|
|
values={{
|
|
path: overrideConfig,
|
|
code: (chunks: string) => <code>{chunks}</code>,
|
|
}}
|
|
/>
|
|
</p>
|
|
<p>
|
|
<FormattedMessage id="setup.welcome_specific_config.next_step" />
|
|
</p>
|
|
</section>
|
|
|
|
<section className="mt-5">
|
|
<div className="d-flex justify-content-center">
|
|
<Button variant="primary mx-2 p-5" onClick={() => onNext()}>
|
|
<FormattedMessage id="actions.next_action" />
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const DefaultWelcomeStep: React.FC<IWizardStep> = ({ 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 (
|
|
<>
|
|
<section>
|
|
<h2 className="mb-5">
|
|
<FormattedMessage id="setup.welcome_to_stash" />
|
|
</h2>
|
|
<p className="lead text-center">
|
|
<FormattedMessage id="setup.welcome.unable_to_locate_config" />
|
|
</p>
|
|
<p>
|
|
<FormattedMessage
|
|
id="setup.welcome.config_path_logic_explained"
|
|
values={{
|
|
code: (chunks: string) => <code>{chunks}</code>,
|
|
fallback_path: fallbackConfigPath,
|
|
}}
|
|
/>
|
|
</p>
|
|
<Alert variant="info text-center">
|
|
<FormattedMessage
|
|
id="setup.welcome.unexpected_explained"
|
|
values={{
|
|
code: (chunks: string) => <code>{chunks}</code>,
|
|
}}
|
|
/>
|
|
</Alert>
|
|
<p>
|
|
<FormattedMessage id="setup.welcome.next_step" />
|
|
</p>
|
|
</section>
|
|
|
|
<section className="mt-5">
|
|
<h3 className="text-center mb-5">
|
|
<FormattedMessage id="setup.welcome.store_stash_config" />
|
|
</h3>
|
|
|
|
<div className="d-flex justify-content-center">
|
|
<Button
|
|
variant="secondary mx-2 p-5"
|
|
onClick={() => onConfigLocationChosen(false)}
|
|
>
|
|
<FormattedMessage
|
|
id="setup.welcome.in_current_stash_directory"
|
|
values={{
|
|
code: (chunks: string) => <code>{chunks}</code>,
|
|
path: fallbackStashDir,
|
|
}}
|
|
/>
|
|
<br />
|
|
<code>{homeDirPath}</code>
|
|
</Button>
|
|
<Button
|
|
variant="secondary mx-2 p-5"
|
|
onClick={() => onConfigLocationChosen(true)}
|
|
disabled={macApp}
|
|
>
|
|
{macApp ? (
|
|
<>
|
|
<FormattedMessage
|
|
id="setup.welcome.in_the_current_working_directory_disabled"
|
|
values={{
|
|
code: (chunks: string) => <code>{chunks}</code>,
|
|
path: pwd,
|
|
}}
|
|
/>
|
|
<br />
|
|
<b>
|
|
<FormattedMessage
|
|
id="setup.welcome.in_the_current_working_directory_disabled_macos"
|
|
values={{
|
|
code: (chunks: string) => <code>{chunks}</code>,
|
|
br: () => <br />,
|
|
}}
|
|
/>
|
|
</b>
|
|
</>
|
|
) : (
|
|
<>
|
|
<FormattedMessage
|
|
id="setup.welcome.in_the_current_working_directory"
|
|
values={{
|
|
code: (chunks: string) => <code>{chunks}</code>,
|
|
path: pwd,
|
|
}}
|
|
/>
|
|
<br />
|
|
<code>{workingDir}</code>
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const WelcomeStep: React.FC<IWizardStep> = (props) => {
|
|
const { systemStatus } = useSetupContext();
|
|
const status = systemStatus?.systemStatus;
|
|
const overrideConfig = status?.configPath;
|
|
|
|
return overrideConfig ? (
|
|
<WelcomeSpecificConfig {...props} />
|
|
) : (
|
|
<DefaultWelcomeStep {...props} />
|
|
);
|
|
};
|
|
|
|
const StashAlert: React.FC<{ close: (confirm: boolean) => void }> = ({
|
|
close,
|
|
}) => {
|
|
const intl = useIntl();
|
|
|
|
return (
|
|
<ModalComponent
|
|
show
|
|
icon={faExclamationTriangle}
|
|
accept={{
|
|
text: intl.formatMessage({ id: "actions.confirm" }),
|
|
variant: "danger",
|
|
onClick: () => close(true),
|
|
}}
|
|
cancel={{ onClick: () => close(false) }}
|
|
>
|
|
<p>
|
|
<FormattedMessage id="setup.paths.stash_alert" />
|
|
</p>
|
|
</ModalComponent>
|
|
);
|
|
};
|
|
|
|
const DatabaseSection: React.FC<{
|
|
databaseFile: string;
|
|
setDatabaseFile: React.Dispatch<React.SetStateAction<string>>;
|
|
}> = ({ databaseFile, setDatabaseFile }) => {
|
|
const intl = useIntl();
|
|
|
|
return (
|
|
<Form.Group id="database">
|
|
<h3>
|
|
<FormattedMessage id="setup.paths.where_can_stash_store_its_database" />
|
|
</h3>
|
|
<p>
|
|
<FormattedMessage
|
|
id="setup.paths.where_can_stash_store_its_database_description"
|
|
values={{
|
|
code: (chunks: string) => <code>{chunks}</code>,
|
|
}}
|
|
/>
|
|
<br />
|
|
<FormattedMessage
|
|
id="setup.paths.where_can_stash_store_its_database_warning"
|
|
values={{
|
|
strong: (chunks: string) => <strong>{chunks}</strong>,
|
|
}}
|
|
/>
|
|
</p>
|
|
<Form.Control
|
|
className="text-input"
|
|
defaultValue={databaseFile}
|
|
placeholder={intl.formatMessage({
|
|
id: "setup.paths.database_filename_empty_for_default",
|
|
})}
|
|
onChange={(e) => setDatabaseFile(e.currentTarget.value)}
|
|
/>
|
|
</Form.Group>
|
|
);
|
|
};
|
|
|
|
const DirectorySelector: React.FC<{
|
|
value: string;
|
|
setValue: React.Dispatch<React.SetStateAction<string>>;
|
|
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 ? (
|
|
<FolderSelectDialog onClose={onSelectClosed} />
|
|
) : null}
|
|
<InputGroup>
|
|
<Form.Control
|
|
className="text-input"
|
|
value={disabled ? "" : value}
|
|
placeholder={placeholder}
|
|
onChange={(e) => setValue(e.currentTarget.value)}
|
|
disabled={disabled}
|
|
/>
|
|
<InputGroup.Append>
|
|
<Button
|
|
variant="secondary"
|
|
className="text-input"
|
|
onClick={() => setShowSelectDialog(true)}
|
|
disabled={disabled}
|
|
>
|
|
<Icon icon={faEllipsisH} />
|
|
</Button>
|
|
</InputGroup.Append>
|
|
</InputGroup>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const GeneratedSection: React.FC<{
|
|
generatedLocation: string;
|
|
setGeneratedLocation: React.Dispatch<React.SetStateAction<string>>;
|
|
}> = ({ generatedLocation, setGeneratedLocation }) => {
|
|
const intl = useIntl();
|
|
|
|
return (
|
|
<Form.Group id="generated">
|
|
<h3>
|
|
<FormattedMessage id="setup.paths.where_can_stash_store_its_generated_content" />
|
|
</h3>
|
|
<p>
|
|
<FormattedMessage
|
|
id="setup.paths.where_can_stash_store_its_generated_content_description"
|
|
values={{
|
|
code: (chunks: string) => <code>{chunks}</code>,
|
|
}}
|
|
/>
|
|
</p>
|
|
<DirectorySelector
|
|
value={generatedLocation}
|
|
setValue={setGeneratedLocation}
|
|
placeholder={intl.formatMessage({
|
|
id: "setup.paths.path_to_generated_directory_empty_for_default",
|
|
})}
|
|
/>
|
|
</Form.Group>
|
|
);
|
|
};
|
|
|
|
const CacheSection: React.FC<{
|
|
cacheLocation: string;
|
|
setCacheLocation: React.Dispatch<React.SetStateAction<string>>;
|
|
}> = ({ cacheLocation, setCacheLocation }) => {
|
|
const intl = useIntl();
|
|
|
|
return (
|
|
<Form.Group id="cache">
|
|
<h3>
|
|
<FormattedMessage id="setup.paths.where_can_stash_store_cache_files" />
|
|
</h3>
|
|
<p>
|
|
<FormattedMessage
|
|
id="setup.paths.where_can_stash_store_cache_files_description"
|
|
values={{
|
|
code: (chunks: string) => <code>{chunks}</code>,
|
|
}}
|
|
/>
|
|
</p>
|
|
<DirectorySelector
|
|
value={cacheLocation}
|
|
setValue={setCacheLocation}
|
|
placeholder={intl.formatMessage({
|
|
id: "setup.paths.path_to_cache_directory_empty_for_default",
|
|
})}
|
|
/>
|
|
</Form.Group>
|
|
);
|
|
};
|
|
|
|
const BlobsSection: React.FC<{
|
|
blobsLocation: string;
|
|
setBlobsLocation: React.Dispatch<React.SetStateAction<string>>;
|
|
storeBlobsInDatabase: boolean;
|
|
setStoreBlobsInDatabase: React.Dispatch<React.SetStateAction<boolean>>;
|
|
}> = ({
|
|
blobsLocation,
|
|
setBlobsLocation,
|
|
storeBlobsInDatabase,
|
|
setStoreBlobsInDatabase,
|
|
}) => {
|
|
const intl = useIntl();
|
|
|
|
return (
|
|
<Form.Group id="blobs">
|
|
<h3>
|
|
<FormattedMessage id="setup.paths.where_can_stash_store_blobs" />
|
|
</h3>
|
|
<p>
|
|
<FormattedMessage
|
|
id="setup.paths.where_can_stash_store_blobs_description"
|
|
values={{
|
|
code: (chunks: string) => <code>{chunks}</code>,
|
|
}}
|
|
/>
|
|
</p>
|
|
<p>
|
|
<FormattedMessage
|
|
id="setup.paths.where_can_stash_store_blobs_description_addendum"
|
|
values={{
|
|
code: (chunks: string) => <code>{chunks}</code>,
|
|
strong: (chunks: string) => <strong>{chunks}</strong>,
|
|
}}
|
|
/>
|
|
</p>
|
|
|
|
<div>
|
|
<Form.Check
|
|
id="store-blobs-in-database"
|
|
checked={storeBlobsInDatabase}
|
|
label={intl.formatMessage({
|
|
id: "setup.paths.store_blobs_in_database",
|
|
})}
|
|
onChange={() => setStoreBlobsInDatabase(!storeBlobsInDatabase)}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<DirectorySelector
|
|
value={blobsLocation}
|
|
setValue={setBlobsLocation}
|
|
placeholder={intl.formatMessage({
|
|
id: "setup.paths.path_to_blobs_directory_empty_for_default",
|
|
})}
|
|
disabled={storeBlobsInDatabase}
|
|
/>
|
|
</div>
|
|
</Form.Group>
|
|
);
|
|
};
|
|
|
|
const SetPathsStep: React.FC<IWizardStep> = ({ goBack, next }) => {
|
|
const { configuration } = useSetupContext();
|
|
|
|
const [showStashAlert, setShowStashAlert] = useState(false);
|
|
|
|
const [stashes, setStashes] = useState<GQL.StashConfig[]>([]);
|
|
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<GQL.SetupInput> = {
|
|
stashes,
|
|
databaseFile,
|
|
generatedLocation,
|
|
cacheLocation,
|
|
blobsLocation: storeBlobsInDatabase ? "" : blobsLocation,
|
|
storeBlobsInDatabase,
|
|
};
|
|
next(input);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{showStashAlert ? (
|
|
<StashAlert
|
|
close={(confirm) => {
|
|
setShowStashAlert(false);
|
|
if (confirm) {
|
|
onNext();
|
|
}
|
|
}}
|
|
/>
|
|
) : null}
|
|
<section>
|
|
<h2 className="mb-3">
|
|
<FormattedMessage id="setup.paths.set_up_your_paths" />
|
|
</h2>
|
|
<p>
|
|
<FormattedMessage id="setup.paths.description" />
|
|
</p>
|
|
</section>
|
|
<section>
|
|
<Form.Group id="stashes">
|
|
<h3>
|
|
<FormattedMessage id="setup.paths.where_is_your_porn_located" />
|
|
</h3>
|
|
<p>
|
|
<FormattedMessage id="setup.paths.where_is_your_porn_located_description" />
|
|
</p>
|
|
<Card>
|
|
<StashConfiguration
|
|
stashes={stashes}
|
|
setStashes={(s) => setStashes(s)}
|
|
/>
|
|
</Card>
|
|
</Form.Group>
|
|
{overrideDatabase ? null : (
|
|
<DatabaseSection
|
|
databaseFile={databaseFile}
|
|
setDatabaseFile={setDatabaseFile}
|
|
/>
|
|
)}
|
|
{overrideGenerated ? null : (
|
|
<GeneratedSection
|
|
generatedLocation={generatedLocation}
|
|
setGeneratedLocation={setGeneratedLocation}
|
|
/>
|
|
)}
|
|
{overrideCache ? null : (
|
|
<CacheSection
|
|
cacheLocation={cacheLocation}
|
|
setCacheLocation={setCacheLocation}
|
|
/>
|
|
)}
|
|
{overrideBlobs ? null : (
|
|
<BlobsSection
|
|
blobsLocation={blobsLocation}
|
|
setBlobsLocation={setBlobsLocation}
|
|
storeBlobsInDatabase={storeBlobsInDatabase}
|
|
setStoreBlobsInDatabase={setStoreBlobsInDatabase}
|
|
/>
|
|
)}
|
|
</section>
|
|
<section className="mt-5">
|
|
<div className="d-flex justify-content-center">
|
|
<Button variant="secondary mx-2 p-5" onClick={() => goBack()}>
|
|
<FormattedMessage id="actions.previous_action" />
|
|
</Button>
|
|
<Button variant="primary mx-2 p-5" onClick={() => preNext()}>
|
|
<FormattedMessage id="actions.next_action" />
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
</>
|
|
);
|
|
};
|
|
|
|
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 <span>{`(excludes ${excludes.join(" and ")})`}</span>;
|
|
};
|
|
|
|
const ConfirmStep: React.FC<IWizardStep> = ({ 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 (
|
|
<>
|
|
<section>
|
|
<h2 className="mb-3">
|
|
<FormattedMessage id="setup.confirm.nearly_there" />
|
|
</h2>
|
|
<p>
|
|
<FormattedMessage id="setup.confirm.almost_ready" />
|
|
</p>
|
|
<dl>
|
|
<dt>
|
|
<FormattedMessage id="setup.confirm.configuration_file_location" />
|
|
</dt>
|
|
<dd>
|
|
<code>{cfgFile}</code>
|
|
</dd>
|
|
</dl>
|
|
<dl>
|
|
<dt>
|
|
<FormattedMessage id="setup.confirm.stash_library_directories" />
|
|
</dt>
|
|
<dd>
|
|
<ul>
|
|
{stashes.map((s) => (
|
|
<li key={s.path}>
|
|
<code>{s.path} </code>
|
|
<StashExclusions stash={s} />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</dd>
|
|
</dl>
|
|
{!overrideDatabase && (
|
|
<dl>
|
|
<dt>
|
|
<FormattedMessage id="setup.confirm.database_file_path" />
|
|
</dt>
|
|
<dd>
|
|
<code>{databaseFile || joinCfgDir("stash-go.sqlite")}</code>
|
|
</dd>
|
|
</dl>
|
|
)}
|
|
{!overrideGenerated && (
|
|
<dl>
|
|
<dt>
|
|
<FormattedMessage id="setup.confirm.generated_directory" />
|
|
</dt>
|
|
<dd>
|
|
<code>{generatedLocation || joinCfgDir("generated")}</code>
|
|
</dd>
|
|
</dl>
|
|
)}
|
|
{!overrideCache && (
|
|
<dl>
|
|
<dt>
|
|
<FormattedMessage id="setup.confirm.cache_directory" />
|
|
</dt>
|
|
<dd>
|
|
<code>{cacheLocation || joinCfgDir("cache")}</code>
|
|
</dd>
|
|
</dl>
|
|
)}
|
|
{!overrideBlobs && (
|
|
<dl>
|
|
<dt>
|
|
<FormattedMessage id="setup.confirm.blobs_directory" />
|
|
</dt>
|
|
<dd>
|
|
<code>
|
|
{storeBlobsInDatabase ? (
|
|
<FormattedMessage id="setup.confirm.blobs_use_database" />
|
|
) : (
|
|
blobsLocation || joinCfgDir("blobs")
|
|
)}
|
|
</code>
|
|
</dd>
|
|
</dl>
|
|
)}
|
|
</section>
|
|
<section className="mt-5">
|
|
<div className="d-flex justify-content-center">
|
|
<Button variant="secondary mx-2 p-5" onClick={() => goBack()}>
|
|
<FormattedMessage id="actions.previous_action" />
|
|
</Button>
|
|
<Button variant="success mx-2 p-5" onClick={() => next()}>
|
|
<FormattedMessage id="actions.confirm" />
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const DiscordLink = (
|
|
<ExternalLink href="https://discord.gg/2TsNFKt">Discord</ExternalLink>
|
|
);
|
|
const GithubLink = (
|
|
<ExternalLink href="https://github.com/stashapp/stash/issues">
|
|
<FormattedMessage id="setup.github_repository" />
|
|
</ExternalLink>
|
|
);
|
|
|
|
const ErrorStep: React.FC<{ error: string; goBack: () => void }> = ({
|
|
error,
|
|
goBack,
|
|
}) => {
|
|
return (
|
|
<>
|
|
<section>
|
|
<h2>
|
|
<FormattedMessage id="setup.errors.something_went_wrong" />
|
|
</h2>
|
|
<p>
|
|
<FormattedMessage
|
|
id="setup.errors.something_went_wrong_while_setting_up_your_system"
|
|
values={{ error: <pre>{error}</pre> }}
|
|
/>
|
|
</p>
|
|
<p>
|
|
<FormattedMessage
|
|
id="setup.errors.something_went_wrong_description"
|
|
values={{ GithubLink, DiscordLink }}
|
|
/>
|
|
</p>
|
|
</section>
|
|
<section className="mt-5">
|
|
<div className="d-flex justify-content-center">
|
|
<Button variant="secondary mx-2 p-5" onClick={goBack}>
|
|
<FormattedMessage id="actions.previous_action" />
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
</>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<>
|
|
<section>
|
|
<h2>
|
|
<FormattedMessage id="setup.success.your_system_has_been_created" />
|
|
</h2>
|
|
<p>
|
|
<FormattedMessage id="setup.success.next_config_step_one" />
|
|
</p>
|
|
<p>
|
|
<FormattedMessage
|
|
id="setup.success.next_config_step_two"
|
|
values={{
|
|
code: (chunks: string) => <code>{chunks}</code>,
|
|
localized_task: intl.formatMessage({
|
|
id: "config.categories.tasks",
|
|
}),
|
|
localized_scan: intl.formatMessage({ id: "actions.scan" }),
|
|
}}
|
|
/>
|
|
</p>
|
|
{!status?.ffmpegPath || !status?.ffprobePath ? (
|
|
<>
|
|
<Alert variant="warning text-center">
|
|
<FormattedMessage
|
|
id="setup.success.missing_ffmpeg"
|
|
values={{
|
|
code: (chunks: string) => <code>{chunks}</code>,
|
|
}}
|
|
/>
|
|
</Alert>
|
|
<p>
|
|
<Form.Check
|
|
id="download-ffmpeg"
|
|
checked={downloadFFmpeg}
|
|
label={intl.formatMessage({
|
|
id: "setup.success.download_ffmpeg",
|
|
})}
|
|
onChange={() => setDownloadFFmpeg(!downloadFFmpeg)}
|
|
/>
|
|
</p>
|
|
</>
|
|
) : null}
|
|
</section>
|
|
<section>
|
|
<h3>
|
|
<FormattedMessage id="setup.success.getting_help" />
|
|
</h3>
|
|
<p>
|
|
<FormattedMessage
|
|
id="setup.success.in_app_manual_explained"
|
|
values={{ icon: <Icon icon={faQuestionCircle} /> }}
|
|
/>
|
|
</p>
|
|
<p>
|
|
<FormattedMessage
|
|
id="setup.success.help_links"
|
|
values={{ DiscordLink, GithubLink }}
|
|
/>
|
|
</p>
|
|
</section>
|
|
<section>
|
|
<h3>
|
|
<FormattedMessage id="setup.success.support_us" />
|
|
</h3>
|
|
<p>
|
|
<FormattedMessage
|
|
id="setup.success.open_collective"
|
|
values={{
|
|
open_collective_link: (
|
|
<ExternalLink href="https://opencollective.com/stashapp">
|
|
Open Collective
|
|
</ExternalLink>
|
|
),
|
|
}}
|
|
/>
|
|
</p>
|
|
<p>
|
|
<FormattedMessage id="setup.success.welcome_contrib" />
|
|
</p>
|
|
</section>
|
|
<section>
|
|
<p className="lead text-center">
|
|
<FormattedMessage id="setup.success.thanks_for_trying_stash" />
|
|
</p>
|
|
</section>
|
|
<section className="mt-5">
|
|
<div className="d-flex justify-content-center">
|
|
<Button variant="success mx-2 p-5" onClick={() => onFinishClick()}>
|
|
<FormattedMessage id="actions.finish" />
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const FinishStep: React.FC<IWizardStep> = ({ goBack }) => {
|
|
const { setupError } = useSetupContext();
|
|
|
|
if (setupError !== undefined) {
|
|
return <ErrorStep error={setupError} goBack={goBack} />;
|
|
}
|
|
|
|
return <SuccessStep />;
|
|
};
|
|
|
|
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<Partial<GQL.SetupInput>>({});
|
|
const [creating, setCreating] = useState(false);
|
|
const [setupError, setSetupError] = useState<string | undefined>(undefined);
|
|
|
|
const history = useHistory();
|
|
|
|
const steps: React.FC<IWizardStep>[] = [
|
|
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<GQL.SetupInput>) {
|
|
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 <LoadingIndicator />;
|
|
}
|
|
|
|
if (
|
|
step === 0 &&
|
|
systemStatus &&
|
|
systemStatus.systemStatus.status !== GQL.SystemStatusEnum.Setup
|
|
) {
|
|
// redirect to main page
|
|
history.push("/");
|
|
return <LoadingIndicator />;
|
|
}
|
|
|
|
if (statusError) {
|
|
return (
|
|
<Container>
|
|
<Alert variant="danger">
|
|
<FormattedMessage
|
|
id="setup.errors.unable_to_retrieve_system_status"
|
|
values={{ error: statusError.message }}
|
|
/>
|
|
</Alert>
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
if (!configuration || !systemStatus) {
|
|
return (
|
|
<Container>
|
|
<Alert variant="danger">
|
|
<FormattedMessage
|
|
id="setup.errors.unable_to_retrieve_configuration"
|
|
values={{ error: "configuration or systemStatus === undefined" }}
|
|
/>
|
|
</Alert>
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SetupContext
|
|
setupState={setupInput}
|
|
setupError={setupError}
|
|
configuration={configuration}
|
|
systemStatus={systemStatus}
|
|
>
|
|
<Container className="setup-wizard">
|
|
<h1 className="text-center">
|
|
<FormattedMessage id="setup.stash_setup_wizard" />
|
|
</h1>
|
|
<Card>
|
|
{creating ? (
|
|
<LoadingIndicator
|
|
message={intl.formatMessage({
|
|
id: "setup.creating.creating_your_system",
|
|
})}
|
|
/>
|
|
) : (
|
|
<Step next={next} goBack={goBack} />
|
|
)}
|
|
</Card>
|
|
</Container>
|
|
</SetupContext>
|
|
);
|
|
};
|
|
|
|
export default Setup;
|