mirror of
https://github.com/System-End/Vencord.git
synced 2026-04-19 20:55:13 +00:00
Cloud Sync: Add sync direction option for single source of truth (#3834)
Co-authored-by: V <vendicated@riseup.net>
This commit is contained in:
parent
ea271496a3
commit
bfc356b69e
6 changed files with 265 additions and 120 deletions
|
|
@ -39,7 +39,7 @@ import { get as dsGet } from "./api/DataStore";
|
|||
import { NotificationData, showNotification } from "./api/Notifications";
|
||||
import { initPluginManager, PMLogger, startAllPlugins } from "./api/PluginManager";
|
||||
import { PlainSettings, Settings, SettingsStore } from "./api/Settings";
|
||||
import { getCloudSettings, putCloudSettings } from "./api/SettingsSync/cloudSync";
|
||||
import { getCloudSettings, putCloudSettings, shouldCloudSync } from "./api/SettingsSync/cloudSync";
|
||||
import { localStorage } from "./utils/localStorage";
|
||||
import { relaunch } from "./utils/native";
|
||||
import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
|
||||
|
|
@ -52,6 +52,11 @@ if (IS_REPORTER) {
|
|||
}
|
||||
|
||||
async function syncSettings() {
|
||||
if (localStorage.Vencord_cloudSyncDirection === undefined) {
|
||||
// by default, sync bi-directionally
|
||||
localStorage.Vencord_cloudSyncDirection = "both";
|
||||
}
|
||||
|
||||
// pre-check for local shared settings
|
||||
if (
|
||||
Settings.cloud.authenticated &&
|
||||
|
|
@ -70,12 +75,12 @@ async function syncSettings() {
|
|||
|
||||
if (
|
||||
Settings.cloud.settingsSync && // if it's enabled
|
||||
Settings.cloud.authenticated // if cloud integrations are enabled
|
||||
Settings.cloud.authenticated && // if cloud integrations are enabled
|
||||
localStorage.Vencord_cloudSyncDirection !== "manual" // if we're not in manual mode
|
||||
) {
|
||||
if (localStorage.Vencord_settingsDirty) {
|
||||
if (localStorage.Vencord_settingsDirty && shouldCloudSync("push")) {
|
||||
await putCloudSettings();
|
||||
delete localStorage.Vencord_settingsDirty;
|
||||
} else if (await getCloudSettings(false)) { // if we synchronized something (false means no sync)
|
||||
} else if (shouldCloudSync("pull") && await getCloudSettings(false)) { // if we synchronized something (false means no sync)
|
||||
// we show a notification here instead of allowing getCloudSettings() to show one to declutter the amount of
|
||||
// potential notifications that might occur. getCloudSettings() will always send a notification regardless if
|
||||
// there was an error to notify the user, but besides that we only want to show one notification instead of all
|
||||
|
|
@ -90,9 +95,8 @@ async function syncSettings() {
|
|||
}
|
||||
|
||||
const saveSettingsOnFrequentAction = debounce(async () => {
|
||||
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
|
||||
if (Settings.cloud.settingsSync && Settings.cloud.authenticated && shouldCloudSync("push")) {
|
||||
await putCloudSettings();
|
||||
delete localStorage.Vencord_settingsDirty;
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { showNotification } from "@api/Notifications";
|
||||
import { PlainSettings, Settings } from "@api/Settings";
|
||||
import { localStorage } from "@utils/localStorage";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { deflateSync, inflateSync } from "fflate";
|
||||
|
|
@ -15,6 +16,12 @@ import { exportSettings, importSettings } from "./offline";
|
|||
|
||||
const logger = new Logger("SettingsSync:Cloud", "#39b7e0");
|
||||
|
||||
export function shouldCloudSync(direction: "push" | "pull") {
|
||||
const localDirection = localStorage.Vencord_cloudSyncDirection;
|
||||
|
||||
return localDirection === direction || localDirection === "both";
|
||||
}
|
||||
|
||||
export async function putCloudSettings(manual?: boolean) {
|
||||
const settings = await exportSettings({ minify: true });
|
||||
|
||||
|
|
@ -53,6 +60,8 @@ export async function putCloudSettings(manual?: boolean) {
|
|||
noPersist: true,
|
||||
});
|
||||
}
|
||||
|
||||
delete localStorage.Vencord_settingsDirty;
|
||||
} catch (e: any) {
|
||||
logger.error("Failed to sync up", e);
|
||||
showNotification({
|
||||
|
|
@ -141,6 +150,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
|
|||
noPersist: true
|
||||
});
|
||||
|
||||
delete localStorage.Vencord_settingsDirty;
|
||||
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
logger.error("Failed to sync down", e);
|
||||
|
|
|
|||
|
|
@ -124,11 +124,11 @@
|
|||
}
|
||||
|
||||
.vc-btn-positive {
|
||||
background-color: var(--redesign-button-positive-background);
|
||||
background-color: var(--redesign-button-positive-background, var(--green-430));
|
||||
color: var(--white);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--redesign-button-positive-pressed-background);
|
||||
background-color: var(--redesign-button-positive-pressed-background, var(--green-460));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -542,3 +542,31 @@ export function VesktopSettingsIcon(props: IconProps) {
|
|||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function CloudDownloadIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M6.5 20Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.83 7.13 7.39 5.75 8.95 4.38 11 4.08V12.15L9.4 10.6L8 12L12 16L16 12L14.6 10.6L13 12.15V4.08Q15.58 4.43 17.29 6.39 19 8.35 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function CloudUploadIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M11 20H6.5Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.88 6.85 7.75 5.43 9.63 4 12 4 14.93 4 16.96 6.04 19 8.07 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20H13V12.85L14.6 14.4L16 13L12 9L8 13L9.4 14.4L11 12.85Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1em;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.vc-cloud-erase-data-danger-btn {
|
||||
|
|
@ -50,6 +51,16 @@
|
|||
background-color: var(--redesign-button-danger-background);
|
||||
}
|
||||
|
||||
.vc-cloud-icon-with-button {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
padding-inline: 0.5em 1em;
|
||||
}
|
||||
|
||||
.vc-cloud-button-icon {
|
||||
height: 1.25em;
|
||||
}
|
||||
|
||||
.vc-settings-modal {
|
||||
padding: 1.5em !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,15 +19,23 @@
|
|||
import { useSettings } from "@api/Settings";
|
||||
import { authorizeCloud, deauthorizeCloud } from "@api/SettingsSync/cloudSetup";
|
||||
import { deleteCloudSettings, eraseAllCloudData, getCloudSettings, putCloudSettings } from "@api/SettingsSync/cloudSync";
|
||||
import { BaseText } from "@components/BaseText";
|
||||
import { Button, ButtonProps } from "@components/Button";
|
||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||
import { Divider } from "@components/Divider";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { FormSwitch } from "@components/FormSwitch";
|
||||
import { Grid } from "@components/Grid";
|
||||
import { Heading } from "@components/Heading";
|
||||
import { CloudDownloadIcon, CloudUploadIcon, DeleteIcon, RestartIcon } from "@components/Icons";
|
||||
import { Link } from "@components/Link";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
|
||||
import { localStorage } from "@utils/localStorage";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { Alerts, Button, Forms, Tooltip } from "@webpack/common";
|
||||
import { classes } from "@utils/misc";
|
||||
import { IconComponent } from "@utils/types";
|
||||
import { Alerts, Select, Tooltip } from "@webpack/common";
|
||||
|
||||
function validateUrl(url: string) {
|
||||
try {
|
||||
|
|
@ -38,131 +46,214 @@ function validateUrl(url: string) {
|
|||
}
|
||||
}
|
||||
|
||||
const SectionHeading = ({ text }: { text: string; }) => (
|
||||
<BaseText
|
||||
tag="h5"
|
||||
size="lg"
|
||||
weight="semibold"
|
||||
className={Margins.bottom16}
|
||||
>
|
||||
{text}
|
||||
</BaseText>
|
||||
);
|
||||
|
||||
function ButtonWithIcon({ children, Icon, className, ...buttonProps }: ButtonProps & { Icon: IconComponent; }) {
|
||||
return (
|
||||
<Button {...buttonProps} className={classes("vc-cloud-icon-with-button", className)}>
|
||||
<Icon className={"vc-cloud-button-icon"} />
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function CloudSetupSection() {
|
||||
const { cloud } = useSettings(["cloud.authenticated", "cloud.url"]);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionHeading text="Cloud Integrations" />
|
||||
|
||||
<Paragraph size="md" className={Margins.bottom20}>
|
||||
Vencord comes with a cloud integration that adds goodies like settings sync across devices.
|
||||
It <Link href="https://vencord.dev/cloud/privacy">respects your privacy</Link>, and
|
||||
the <Link href="https://github.com/Vencord/Backend">source code</Link> is AGPL 3.0 licensed so you
|
||||
can host it yourself.
|
||||
</Paragraph>
|
||||
<FormSwitch
|
||||
key="backend"
|
||||
title="Enable Cloud Integrations"
|
||||
description="This will request authorization if you have not yet set up cloud integrations."
|
||||
value={cloud.authenticated}
|
||||
onChange={v => {
|
||||
if (v)
|
||||
authorizeCloud();
|
||||
else
|
||||
cloud.authenticated = v;
|
||||
}}
|
||||
/>
|
||||
<Heading tag="h5" className={Margins.top16}>Backend URL</Heading>
|
||||
<Paragraph className={Margins.bottom8}>
|
||||
Which backend to use when using cloud integrations.
|
||||
</Paragraph>
|
||||
<CheckedTextInput
|
||||
key="backendUrl"
|
||||
value={cloud.url}
|
||||
onChange={async v => {
|
||||
cloud.url = v;
|
||||
cloud.authenticated = false;
|
||||
deauthorizeCloud();
|
||||
}}
|
||||
validate={validateUrl}
|
||||
/>
|
||||
|
||||
<Grid columns={1} gap="1em" className={Margins.top8}>
|
||||
<ButtonWithIcon
|
||||
variant="primary"
|
||||
disabled={!cloud.authenticated}
|
||||
onClick={async () => {
|
||||
await deauthorizeCloud();
|
||||
cloud.authenticated = false;
|
||||
await authorizeCloud();
|
||||
}}
|
||||
Icon={RestartIcon}
|
||||
>
|
||||
Reauthorise
|
||||
</ButtonWithIcon>
|
||||
</Grid>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsSyncSection() {
|
||||
const { cloud } = useSettings(["cloud.authenticated", "cloud.settingsSync"]);
|
||||
const sectionEnabled = cloud.authenticated && cloud.settingsSync;
|
||||
|
||||
return (
|
||||
<section className={Margins.top16}>
|
||||
<Forms.FormTitle tag="h5">Settings Sync</Forms.FormTitle>
|
||||
<section>
|
||||
<SectionHeading text="Settings Sync" />
|
||||
<Flex flexDirection="column" gap="1em">
|
||||
<FormSwitch
|
||||
key="cloud-sync"
|
||||
title="Enable Settings Sync"
|
||||
description="Save your Vencord settings to the cloud so you can easily keep them the same on all your devices"
|
||||
value={cloud.settingsSync}
|
||||
onChange={v => { cloud.settingsSync = v; }}
|
||||
disabled={!cloud.authenticated}
|
||||
hideBorder
|
||||
/>
|
||||
|
||||
<Paragraph size="md" className={Margins.bottom20}>
|
||||
Synchronize your settings to the cloud. This allows easy synchronization across multiple devices with
|
||||
minimal effort.
|
||||
</Paragraph>
|
||||
<FormSwitch
|
||||
key="cloud-sync"
|
||||
title="Settings Sync"
|
||||
value={cloud.settingsSync}
|
||||
onChange={v => { cloud.settingsSync = v; }}
|
||||
disabled={!cloud.authenticated}
|
||||
/>
|
||||
<div className="vc-cloud-settings-sync-grid">
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={!sectionEnabled}
|
||||
onClick={() => putCloudSettings(true)}
|
||||
>
|
||||
Sync to Cloud
|
||||
</Button>
|
||||
<Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!">
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<Button
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.RED}
|
||||
disabled={!sectionEnabled}
|
||||
onClick={() => getCloudSettings(true, true)}
|
||||
>
|
||||
Sync from Cloud
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.RED}
|
||||
disabled={!sectionEnabled}
|
||||
<div>
|
||||
<Heading tag="h5">
|
||||
Sync Rules for This Device
|
||||
</Heading>
|
||||
<Paragraph className={Margins.bottom8}>
|
||||
This setting controls how settings move between <strong>this device</strong> and the cloud.
|
||||
You can let changes flow both ways, or choose one place to be the main source of truth.
|
||||
</Paragraph>
|
||||
<Select
|
||||
options={[
|
||||
{
|
||||
label: "Two-way sync (changes go both directions)",
|
||||
value: "both",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: "This device is the source (upload only)",
|
||||
value: "push",
|
||||
},
|
||||
{
|
||||
label: "The cloud is the source (download only)",
|
||||
value: "pull",
|
||||
},
|
||||
{
|
||||
label: "Do not sync automatically (manual sync via buttons below only)",
|
||||
value: "manual",
|
||||
}
|
||||
]}
|
||||
isSelected={v => v === localStorage.Vencord_cloudSyncDirection}
|
||||
serialize={v => String(v)}
|
||||
select={v => {
|
||||
localStorage.Vencord_cloudSyncDirection = v;
|
||||
}}
|
||||
closeOnSelect={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Grid columns={2} gap="1em" className={Margins.top20}>
|
||||
<ButtonWithIcon
|
||||
variant="positive"
|
||||
disabled={!sectionEnabled}
|
||||
onClick={() => putCloudSettings(true)}
|
||||
Icon={CloudUploadIcon}
|
||||
>
|
||||
Upload Settings
|
||||
</ButtonWithIcon>
|
||||
<Tooltip text="This will replace your current settings with the ones saved in the cloud. Be careful!">
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<ButtonWithIcon
|
||||
variant="dangerPrimary"
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
disabled={!sectionEnabled}
|
||||
onClick={() => getCloudSettings(true, true)}
|
||||
Icon={CloudDownloadIcon}
|
||||
>
|
||||
Download Settings
|
||||
</ButtonWithIcon>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Flex>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ResetSection() {
|
||||
const { authenticated, settingsSync } = useSettings(["cloud.authenticated", "cloud.settingsSync"]).cloud;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionHeading text="Reset Cloud Data" />
|
||||
|
||||
<Grid columns={2} gap="1em">
|
||||
<ButtonWithIcon
|
||||
variant="dangerPrimary"
|
||||
disabled={!authenticated || !settingsSync}
|
||||
onClick={() => deleteCloudSettings()}
|
||||
Icon={DeleteIcon}
|
||||
>
|
||||
Delete Cloud Settings
|
||||
</Button>
|
||||
</div>
|
||||
Delete Settings from Cloud
|
||||
</ButtonWithIcon>
|
||||
<ButtonWithIcon
|
||||
variant="dangerPrimary"
|
||||
disabled={!authenticated}
|
||||
onClick={() => Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: "Once your data is erased, we cannot recover it. There's no going back!",
|
||||
onConfirm: eraseAllCloudData,
|
||||
confirmText: "Erase it!",
|
||||
confirmColor: "vc-cloud-erase-data-danger-btn",
|
||||
cancelText: "Nevermind"
|
||||
})}
|
||||
Icon={DeleteIcon}
|
||||
>
|
||||
Delete your Cloud Account
|
||||
</ButtonWithIcon>
|
||||
</Grid>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function CloudTab() {
|
||||
const settings = useSettings(["cloud.authenticated", "cloud.url"]);
|
||||
|
||||
return (
|
||||
<SettingsTab>
|
||||
<section className={Margins.top16}>
|
||||
<Paragraph size="md" className={Margins.bottom20}>
|
||||
Vencord comes with a cloud integration that adds goodies like settings sync across devices.
|
||||
It <Link href="https://vencord.dev/cloud/privacy">respects your privacy</Link>, and
|
||||
the <Link href="https://github.com/Vencord/Backend">source code</Link> is AGPL 3.0 licensed so you
|
||||
can host it yourself.
|
||||
</Paragraph>
|
||||
<FormSwitch
|
||||
key="backend"
|
||||
title="Enable Cloud Integrations"
|
||||
description="This will request authorization if you have not yet set up cloud integrations."
|
||||
value={settings.cloud.authenticated}
|
||||
onChange={v => {
|
||||
if (v)
|
||||
authorizeCloud();
|
||||
else
|
||||
settings.cloud.authenticated = v;
|
||||
}}
|
||||
/>
|
||||
<Forms.FormTitle tag="h5" className={Margins.top16}>Backend URL</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom8}>
|
||||
Which backend to use when using cloud integrations.
|
||||
</Forms.FormText>
|
||||
<CheckedTextInput
|
||||
key="backendUrl"
|
||||
value={settings.cloud.url}
|
||||
onChange={async v => {
|
||||
settings.cloud.url = v;
|
||||
settings.cloud.authenticated = false;
|
||||
deauthorizeCloud();
|
||||
}}
|
||||
validate={validateUrl}
|
||||
/>
|
||||
|
||||
<Grid columns={2} gap="1em" className={Margins.top8}>
|
||||
<Button
|
||||
size={Button.Sizes.MEDIUM}
|
||||
disabled={!settings.cloud.authenticated}
|
||||
onClick={async () => {
|
||||
await deauthorizeCloud();
|
||||
settings.cloud.authenticated = false;
|
||||
await authorizeCloud();
|
||||
}}
|
||||
>
|
||||
Reauthorise
|
||||
</Button>
|
||||
<Button
|
||||
size={Button.Sizes.MEDIUM}
|
||||
color={Button.Colors.RED}
|
||||
disabled={!settings.cloud.authenticated}
|
||||
onClick={() => Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: "Once your data is erased, we cannot recover it. There's no going back!",
|
||||
onConfirm: eraseAllCloudData,
|
||||
confirmText: "Erase it!",
|
||||
confirmColor: "vc-cloud-erase-data-danger-btn",
|
||||
cancelText: "Nevermind"
|
||||
})}
|
||||
>
|
||||
Erase All Data
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Divider className={Margins.top16} />
|
||||
</section >
|
||||
<SettingsSyncSection />
|
||||
<Flex flexDirection="column" gap="1em">
|
||||
<CloudSetupSection />
|
||||
<Divider />
|
||||
<SettingsSyncSection />
|
||||
<Divider />
|
||||
<ResetSection />
|
||||
</Flex>
|
||||
</SettingsTab>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue