import { useState, useEffect } from "react";
import SyncManager from "src/sync-manager";
import { usePlugin } from "src/views/hooks";
import { DEFAULT_SETTINGS } from "src/settings/settings";
const STEPS = [
"welcome",
"repo",
"token",
"folders",
"sync",
"first_sync",
] as const;
type Step = (typeof STEPS)[number];
type StepData = {
repo: { owner: string; repo: string; branch: string };
token: { token: string };
folders: { repoFolder: string; vaultFolder: string };
sync: {
mode: "manual" | "interval";
syncOnStart: boolean;
onConflict: "ignore" | "ask" | "overwrite";
};
};
const DEFAULT_STEP_DATA: StepData = {
repo: { owner: "", repo: "", branch: "" },
token: { token: "" },
folders: { repoFolder: "", vaultFolder: "" },
sync: {
mode: "manual",
syncOnStart: false,
onConflict: "overwrite",
},
};
const WelcomeStepComponent = () => {
return (
First setup
This plugin is still in beta so not all features are available yet. If
you find any issues report that.
You'll be guided through some steps to get you started.
);
};
const RepoStepComponent = ({
values,
onChange,
}: {
values: { owner: string; repo: string; branch: string };
onChange: (values: { owner: string; repo: string; branch: string }) => void;
}) => {
return (
);
};
const TokenStepComponent = ({
values,
onChange,
}: {
values: { token: string };
onChange: (values: { token: string }) => void;
}) => {
return (
Now we need a GitHub Personal Access Token.
The token is used to get, create and update the files between your vault
and your GitHub repository. Click{" "}
here to
create a new token. For more information about the tokens see{" "}
the official GitHub documentation.
The token must have the Contents Read and Write permissions for the same
repository you set in the previous step.
onChange({ token: e.target.value })}
/>
);
};
const FoldersStepComponent = ({
values,
onChange,
}: {
values: { repoFolder: string; vaultFolder: string };
onChange: (values: { repoFolder: string; vaultFolder: string }) => void;
}) => {
return (
);
};
const SyncSettingsStepComponent = ({
values,
onChange,
}: {
values: {
mode: "manual" | "interval";
syncOnStart: boolean;
onConflict: "ignore" | "ask" | "overwrite";
};
onChange: (values: {
mode: "manual" | "interval";
syncOnStart: boolean;
onConflict: "ignore" | "ask" | "overwrite";
}) => void;
}) => {
return (
You can sync your vault manually or every minute. You can change the
interval at a later time.
onChange({
...values,
mode: e.target.value as typeof values.mode,
})
}
>
Manually
On Interval
Optionally you can sync every time you open Obsidian.
onChange({ ...values, syncOnStart: !values.syncOnStart })
}
>
In case of conflicts you can choose how to handle them.
This is not yet implemented so remote files will always overwrite local
files in case of conflicts.
onChange({
...values,
onConflict: e.target.value as typeof values.onConflict,
})
}
>
Ignore remote file
Ask
Overwrite local file
);
};
const FirstSyncStepComponent = ({
stepData,
onClose,
}: {
stepData: StepData;
onClose: () => Promise;
}) => {
const [syncing, setSyncing] = useState(true);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const plugin = usePlugin();
if (!plugin) {
// Unlikely to happen, makes TS happy though
throw new Error("Plugin is not initialized");
}
useEffect(() => {
(async () => {
const settings = {
...DEFAULT_SETTINGS,
githubToken: stepData.token.token,
githubOwner: stepData.repo.owner,
githubRepo: stepData.repo.repo,
githubBranch: stepData.repo.branch,
repoContentDir: stepData.folders.repoFolder,
localContentDir: stepData.folders.vaultFolder,
uploadStrategy: stepData.sync.mode,
syncOnStartup: stepData.sync.syncOnStart,
conflictHandling: stepData.sync.onConflict,
};
// We don't want to save the settings yet, so we create a new sync manager
// used only for the first sync.
// After that's sucessful we'll save the settings.
const syncManager = new SyncManager(
plugin.app.vault,
settings,
// There can't be any conflicts at this point so we don't need to handle them
async (_) => {
return [];
},
);
await syncManager.loadMetadata();
try {
await syncManager.firstSync();
} catch (e) {
setError(e.message);
setSuccess(false);
setSyncing(false);
return;
}
plugin.settings = settings;
await plugin.saveSettings();
setSyncing(false);
setSuccess(true);
})();
}, [stepData]);
const loadingBarStyle = () => {
if (error) {
return { width: 0 };
} else if (success) {
return { width: "100%" };
} else {
return {};
}
};
return (
Now we can try and sync your vault with your repository. This might take
a few minutes depending on the number of files.
{syncing ? "Syncing..." : null}
{error ? "Syncing failed" : null}
{success ? "Syncing complete" : null}
{error ?
{error}
: null}
{success ?
Everything is set up and ready to go.
: null}
{success ? (
await onClose()}
>
Close
) : null}
);
};
const OnBoardingComponent = ({ onClose }: { onClose: () => Promise }) => {
// It starts as true because the first step is just a welcome message
// so the button is already enabled.
const [isValid, setIsValid] = useState(true);
const [step, setStep] = useState("welcome");
const [stepData, setStepData] = useState(DEFAULT_STEP_DATA);
const updateStepData = (step: Step, data: StepData[keyof StepData]) => {
setStepData((stepData) => {
return {
...stepData,
[step]: data,
};
});
};
const isStepValid = (step: Step) => {
switch (step) {
// Only these steps have required data, the others don't
// so the step is automatically valid
case "repo":
const { owner, repo, branch } = stepData.repo ?? {};
return Boolean(owner && repo && branch);
case "token":
return Boolean(stepData.token?.token);
case "sync":
default:
return true;
}
};
useEffect(() => {
setIsValid(isStepValid(step));
}, [step, stepData]);
const previousStep = () => {
setStep((currentStep) => {
const currentIndex = STEPS.indexOf(currentStep);
return STEPS[currentIndex - 1] ?? STEPS[0];
});
};
const nextStep = () => {
setStep((currentStep) => {
const currentIndex = STEPS.indexOf(currentStep);
return STEPS[currentIndex + 1] ?? STEPS[STEPS.length - 1];
});
};
const currentStepComponent = () => {
switch (step) {
case "welcome":
return ;
case "repo":
return (
updateStepData("repo", data)}
/>
);
case "token":
return (
updateStepData("token", data)}
/>
);
case "folders":
return (
updateStepData("folders", data)}
/>
);
case "sync":
return (
updateStepData("sync", data)}
/>
);
case "first_sync":
return ;
}
};
return (
);
};
export default OnBoardingComponent;