mirror of
https://github.com/MadeBaruna/paimon-moe.git
synced 2024-12-22 14:35:38 +01:00
WIP: Drive sync flow
This commit is contained in:
parent
d850e0b921
commit
333a7b6928
10 changed files with 247 additions and 38 deletions
|
@ -27,6 +27,7 @@
|
|||
"@rollup/plugin-replace": "^2.3.4",
|
||||
"@rollup/plugin-url": "^5.0.0",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"dayjs": "^1.9.4",
|
||||
"dotenv": "^8.2.0",
|
||||
"postcss": "^8.1.2",
|
||||
"postcss-load-config": "^3.0.0",
|
||||
|
@ -37,6 +38,7 @@
|
|||
"sapper": "^0.28.0",
|
||||
"svelte": "^3.17.3",
|
||||
"svelte-preprocess": "^4.5.1",
|
||||
"svelte-simple-modal": "^0.6.1",
|
||||
"tailwindcss": "^1.9.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { driveSignedIn, driveLoading } from '../stores/dataSync';
|
||||
// doc: /static/images.save_sync_flow.png
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { driveSignedIn, driveLoading, saveId } from '../stores/dataSync';
|
||||
import { getLocalSaveJson, updateSave, updateTime, UPDATE_TIME_KEY } from '../stores/saveManager';
|
||||
|
||||
import SyncConflictModal from '../components/SyncConflictModal.svelte';
|
||||
|
||||
const { open: openModal } = getContext('simple-modal');
|
||||
|
||||
const CLIENT_ID = __paimon.env.GOOGLE_DRIVE_CLIENT_ID;
|
||||
const API_KEY = __paimon.env.GOOGLE_DRIVE_API_KEY;
|
||||
const DISCOVERY_DOCS = ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'];
|
||||
const SCOPES = 'https://www.googleapis.com/auth/drive.appdata';
|
||||
|
||||
$: localSaveExists = $updateTime !== null;
|
||||
|
||||
onMount(() => {
|
||||
const script = document.createElement('script');
|
||||
script.onload = handleClientLoad;
|
||||
|
@ -19,7 +29,7 @@
|
|||
}
|
||||
|
||||
function updateSigninStatus(status) {
|
||||
console.log('DRIVE signed in:', status);
|
||||
console.log('update drive signed in status:', status);
|
||||
driveSignedIn.set(status);
|
||||
driveLoading.set(false);
|
||||
|
||||
|
@ -28,7 +38,46 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function copyRemoteToLocal() {
|
||||
try {
|
||||
const remoteSaveData = await getData();
|
||||
for (const k in remoteSaveData) {
|
||||
updateSave(k, remoteSaveData[k], true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function compareLocalSave() {
|
||||
try {
|
||||
const data = await getData();
|
||||
const remoteTime = dayjs(data[UPDATE_TIME_KEY]);
|
||||
if ($updateTime !== null && remoteTime.diff($updateTime) !== 0) {
|
||||
console.log('DRIVE SYNC CONFLICT!');
|
||||
openModal(
|
||||
SyncConflictModal,
|
||||
{
|
||||
remoteTime: remoteTime,
|
||||
localTime: $updateTime,
|
||||
},
|
||||
{
|
||||
closeButton: false,
|
||||
closeOnEsc: false,
|
||||
closeOnOuterClick: false,
|
||||
styleWindow: { background: '#25294A' },
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// check if remote save.json exists
|
||||
async function getFiles() {
|
||||
console.log('checking remote file');
|
||||
|
||||
try {
|
||||
const { result } = await gapi.client.drive.files.list({
|
||||
spaces: 'appDataFolder',
|
||||
|
@ -36,14 +85,17 @@
|
|||
});
|
||||
console.log(result);
|
||||
|
||||
// create save.json on remote if not exists
|
||||
if (result.files.length === 0) {
|
||||
createFile();
|
||||
await createFile();
|
||||
} else {
|
||||
const data = await gapi.client.drive.files.get({
|
||||
fileId: result.files[0].id,
|
||||
alt: 'media',
|
||||
});
|
||||
console.log(data);
|
||||
saveId.set(result.files[0].id);
|
||||
|
||||
if (localSaveExists) {
|
||||
await compareLocalSave();
|
||||
} else {
|
||||
await copyRemoteToLocal();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
@ -51,6 +103,8 @@
|
|||
}
|
||||
|
||||
async function createFile() {
|
||||
console.log('creating remote file');
|
||||
|
||||
try {
|
||||
const { result } = await gapi.client.drive.files.create({
|
||||
resource: {
|
||||
|
@ -60,16 +114,45 @@
|
|||
fields: 'id',
|
||||
});
|
||||
|
||||
const data = await gapi.client.request({
|
||||
path: `/upload/drive/v3/files/${result.id}`,
|
||||
saveId.set(result.id);
|
||||
|
||||
if (localSaveExists) {
|
||||
await saveData(getLocalSaveJson());
|
||||
}
|
||||
|
||||
console.log(result);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function getData() {
|
||||
console.log('reading remote file');
|
||||
|
||||
try {
|
||||
const { result } = await gapi.client.drive.files.get({
|
||||
fileId: $saveId,
|
||||
alt: 'media',
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveData(data) {
|
||||
console.log('saving to remote file');
|
||||
|
||||
try {
|
||||
await gapi.client.request({
|
||||
path: `/upload/drive/v3/files/${$saveId}`,
|
||||
method: 'PATCH',
|
||||
params: {
|
||||
uploadType: 'media',
|
||||
},
|
||||
body: JSON.stringify({ v: __paimon.env.CURRENT_VERSION }),
|
||||
body: data,
|
||||
});
|
||||
|
||||
console.log(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
|
48
src/components/SyncConflictModal.svelte
Normal file
48
src/components/SyncConflictModal.svelte
Normal file
|
@ -0,0 +1,48 @@
|
|||
<script>
|
||||
import { mdiCloudAlert, mdiContentSave, mdiDownload, mdiFile, mdiGoogleDrive, mdiUpload } from '@mdi/js';
|
||||
import Button from './Button.svelte';
|
||||
|
||||
import Icon from './Icon.svelte';
|
||||
|
||||
export let remoteTime;
|
||||
export let localTime;
|
||||
|
||||
const remoteFormatted = remoteTime.format('dddd, MMMM D, YYYY h:mm A');
|
||||
const localFormatted = localTime.format('dddd, MMMM D, YYYY h:mm A');
|
||||
const remoteNewer = remoteTime.isAfter(localTime);
|
||||
</script>
|
||||
|
||||
<div class="flex">
|
||||
<Icon path={mdiCloudAlert} color="white" size={2} />
|
||||
<p class="flex-1 text-white text-lg md:text-xl font-bold ml-4">
|
||||
Your local data is conflicting with the ones stored in the Google Drive!
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 p-4 bg-item rounded-xl text-white">
|
||||
<p class="font-bold">
|
||||
<Icon path={mdiGoogleDrive} className="mr-2" />
|
||||
Google Drive Data -
|
||||
{remoteNewer ? 'NEWER' : 'OLDER'}
|
||||
</p>
|
||||
<p class="text-gray-400 mt-1">Last modified: {remoteFormatted}</p>
|
||||
<Button className="mt-2 w-full">
|
||||
<Icon path={mdiDownload} className="mr-1" />Replace Local Data
|
||||
</Button>
|
||||
</div>
|
||||
<p class="mt-2 text-white text-center">OR</p>
|
||||
<div class="mt-2 p-4 bg-item rounded-xl text-white">
|
||||
<p class="font-bold">
|
||||
<Icon path={mdiFile} className="mr-2" />
|
||||
Local Data -
|
||||
{remoteNewer ? 'OLDER' : 'NEWER'}
|
||||
</p>
|
||||
<p class="text-gray-400 mt-1">Last modified: {localFormatted}</p>
|
||||
<Button className="mt-2 w-full">
|
||||
<Icon path={mdiUpload} className="mr-1" />Replace Google Drive Data
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex mt-6 justify-end">
|
||||
<Button className="w-full md:w-auto">
|
||||
<Icon path={mdiContentSave} className="mr-1" />Download Both Data
|
||||
</Button>
|
||||
</div>
|
|
@ -1,12 +1,21 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import Modal from 'svelte-simple-modal';
|
||||
|
||||
import Tailwind from '../components/Tailwindcss.svelte';
|
||||
import Sidebar from '../components/Sidebar/Sidebar.svelte';
|
||||
import Header from '../components/Header.svelte';
|
||||
import DataSync from '../components/DataSync.svelte';
|
||||
|
||||
import { showSidebar } from '../stores/sidebar';
|
||||
import { checkLocalSave } from '../stores/saveManager';
|
||||
|
||||
export let segment;
|
||||
|
||||
// check local storage save on load
|
||||
onMount(() => {
|
||||
checkLocalSave();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
@ -20,12 +29,13 @@
|
|||
{#if $showSidebar}
|
||||
<Sidebar {segment} mobile />
|
||||
{/if}
|
||||
<main style="flex: 1 0 auto;">
|
||||
<slot />
|
||||
</main>
|
||||
<Modal>
|
||||
<main style="flex: 1 0 auto;">
|
||||
<slot />
|
||||
</main>
|
||||
<DataSync />
|
||||
</Modal>
|
||||
<p class="lg:ml-64 px-8 py-4 text-gray-600">
|
||||
Paimon.moe is not affiliated with miHoYo.<br />
|
||||
Genshin Impact, game content and materials are trademarks and copyrights of miHoYo.
|
||||
</p>
|
||||
|
||||
<DataSync />
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
import { mdiGoogleDrive, mdiLoading } from '@mdi/js';
|
||||
import { mdiCheckCircleOutline, mdiGoogleDrive, mdiLoading } from '@mdi/js';
|
||||
|
||||
import Button from '../components/Button.svelte';
|
||||
import Icon from '../components/Icon.svelte';
|
||||
import { driveSignedIn, driveLoading } from '../stores/dataSync';
|
||||
import { driveSignedIn, driveLoading, synced } from '../stores/dataSync';
|
||||
|
||||
function signIn() {
|
||||
gapi.auth2.getAuthInstance().signIn();
|
||||
|
@ -15,20 +15,32 @@
|
|||
</script>
|
||||
|
||||
<div class="lg:ml-64 pt-20 px-8 lg:pt-8">
|
||||
<p class="text-white mb-2">
|
||||
Paimon.moe use Application Data Directory on your Google Drive to save and sync your wish counter and todo list.
|
||||
</p>
|
||||
<p class="text-white mb-4">Paimon.moe can only read and write file that this site create.</p>
|
||||
{#if $driveLoading}
|
||||
<Icon path={mdiLoading} color="white" spin />
|
||||
{:else if !$driveSignedIn}
|
||||
<Button on:click={signIn}>
|
||||
<Icon path={mdiGoogleDrive} className="mr-2" />
|
||||
Sign in to Google Drive
|
||||
</Button>
|
||||
{:else}
|
||||
<p class="text-white">Drive signed in: {$driveSignedIn}</p>
|
||||
{#if !$driveSignedIn}
|
||||
<Button on:click={signIn}>
|
||||
<Icon path={mdiGoogleDrive} className="mr-2" />
|
||||
Sign in to Google Drive
|
||||
</Button>
|
||||
{:else}
|
||||
<Button on:click={signOut}>
|
||||
<Icon path={mdiGoogleDrive} className="mr-2" />
|
||||
Sign out Google Drive
|
||||
</Button>
|
||||
{/if}
|
||||
<Button on:click={signOut}>
|
||||
<Icon path={mdiGoogleDrive} className="mr-2" />
|
||||
Sign out Google Drive
|
||||
</Button>
|
||||
<p class="text-white mt-4">
|
||||
Sync Status:
|
||||
<span class={`font-bold ${$synced ? 'text-green-400' : 'text-yellow-400'}`}>
|
||||
{$synced ? 'Synced' : 'Syncing...'}
|
||||
{#if $synced}
|
||||
<Icon path={mdiCheckCircleOutline} className="text-green-400" />
|
||||
{:else}
|
||||
<Icon path={mdiLoading} className="text-yellow-400" spin />
|
||||
{/if}
|
||||
</span>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<script>
|
||||
import { mdiStar } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { mdiStar } from '@mdi/js';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import Button from '../../components/Button.svelte';
|
||||
import Icon from '../../components/Icon.svelte';
|
||||
import { readSave, updateSave, fromRemote } from '../../stores/saveManager';
|
||||
|
||||
export let id = '';
|
||||
export let name = '';
|
||||
|
@ -12,13 +14,16 @@
|
|||
let rare = 0;
|
||||
|
||||
$: path = `wish-counter-${id}`;
|
||||
$: if ($fromRemote) {
|
||||
readLocalData();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
readLocalData();
|
||||
});
|
||||
|
||||
function readLocalData() {
|
||||
const data = localStorage.getItem(path);
|
||||
const data = readSave(path);
|
||||
if (data !== null) {
|
||||
const counterData = JSON.parse(data);
|
||||
total = counterData.total;
|
||||
|
@ -27,14 +32,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
function saveData() {
|
||||
const saveData = debounce(() => {
|
||||
const data = JSON.stringify({
|
||||
total,
|
||||
legendary,
|
||||
rare,
|
||||
});
|
||||
localStorage.setItem(path, data);
|
||||
}
|
||||
|
||||
updateSave(path, data);
|
||||
}, 2000);
|
||||
|
||||
function add(val) {
|
||||
if (total + val < 0) return;
|
||||
|
|
|
@ -2,3 +2,5 @@ import { writable } from 'svelte/store';
|
|||
|
||||
export const driveSignedIn = writable(false);
|
||||
export const driveLoading = writable(true);
|
||||
export const synced = writable(true);
|
||||
export const saveId = writable('');
|
||||
|
|
36
src/stores/saveManager.js
Normal file
36
src/stores/saveManager.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import dayjs from 'dayjs';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const updateTime = writable(null);
|
||||
export const fromRemote = writable(false);
|
||||
|
||||
export const UPDATE_TIME_KEY = 'update-time';
|
||||
|
||||
export const checkLocalSave = () => {
|
||||
const localUpdateTime = localStorage.getItem(UPDATE_TIME_KEY);
|
||||
if (localUpdateTime !== null) {
|
||||
updateTime.set(dayjs(localUpdateTime));
|
||||
console.log('local save update time:', localUpdateTime);
|
||||
}
|
||||
};
|
||||
|
||||
export const getLocalSaveJson = () => {
|
||||
return JSON.stringify(localStorage);
|
||||
};
|
||||
|
||||
export const updateSave = (key, data, isFromRemote) => {
|
||||
localStorage.setItem(key, data);
|
||||
|
||||
if (!isFromRemote) {
|
||||
const currentTime = dayjs().toISOString();
|
||||
updateTime.set(currentTime);
|
||||
localStorage.setItem(UPDATE_TIME_KEY, currentTime);
|
||||
} else {
|
||||
fromRemote.set(true);
|
||||
}
|
||||
};
|
||||
|
||||
export const readSave = (key) => {
|
||||
const data = localStorage.getItem(key);
|
||||
return data;
|
||||
};
|
BIN
static/images/save_sync_flow.png
Normal file
BIN
static/images/save_sync_flow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
10
yarn.lock
10
yarn.lock
|
@ -1278,6 +1278,11 @@ cssesc@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
|
||||
|
||||
dayjs@^1.9.4:
|
||||
version "1.9.4"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.9.4.tgz#fcde984e227f4296f04e7b05720adad2e1071f1b"
|
||||
integrity sha512-ABSF3alrldf7nM9sQ2U+Ln67NRwmzlLOqG7kK03kck0mw3wlSSEKv/XhKGGxUjQcS57QeiCyNdrFgtj9nWlrng==
|
||||
|
||||
debug@2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
|
@ -2386,6 +2391,11 @@ svelte-preprocess@^4.5.1:
|
|||
detect-indent "^6.0.0"
|
||||
strip-indent "^3.0.0"
|
||||
|
||||
svelte-simple-modal@^0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/svelte-simple-modal/-/svelte-simple-modal-0.6.1.tgz#5e984f384dda16bc50f00846314dc140ad89864b"
|
||||
integrity sha512-GJGYj+jymzuar105fwkZ73dtcSFCordpbHqt53iE1N1GdqhvEmSs24idRzyIcO7TrTD/V/287X1icFXp88RQHQ==
|
||||
|
||||
svelte@^3.17.3:
|
||||
version "3.29.0"
|
||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.29.0.tgz#80acac4254341ad8f3301e5ef03f4127ea967d96"
|
||||
|
|
Loading…
Reference in a new issue