420 lines
12 KiB
Svelte
420 lines
12 KiB
Svelte
<script lang="ts">
|
|
import { processFiles } from '$lib/upload';
|
|
import Loader from '$lib/components/Loader.svelte';
|
|
import { formatBytes, showSnackbar, uploadFile } from '$lib/util';
|
|
import { Button, P, Progressbar } from 'flowbite-svelte';
|
|
import { onMount } from 'svelte';
|
|
import { promptConfirmation } from '$lib/util';
|
|
import { GoogleSolid } from 'flowbite-svelte-icons';
|
|
import { profiles, volumes } from '$lib/settings';
|
|
|
|
const CLIENT_ID = import.meta.env.VITE_GDRIVE_CLIENT_ID;
|
|
const API_KEY = import.meta.env.VITE_GDRIVE_API_KEY;
|
|
|
|
const DISCOVERY_DOC = 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest';
|
|
const SCOPES = 'https://www.googleapis.com/auth/drive.file';
|
|
|
|
const FOLDER_MIME_TYPE = 'application/vnd.google-apps.folder';
|
|
const READER_FOLDER = 'mokuro-reader';
|
|
const VOLUME_DATA_FILE = 'volume-data.json';
|
|
const PROFILES_FILE = 'profiles.json';
|
|
|
|
const type = 'application/json';
|
|
|
|
let tokenClient: any;
|
|
let accessToken = '';
|
|
let gapiLoaded = false;
|
|
let googleLoaded = false;
|
|
|
|
let readerFolderId = '';
|
|
let volumeDataId = '';
|
|
let profilesId = '';
|
|
|
|
let loadingMessage = '';
|
|
let errorMessage = '';
|
|
|
|
let completed = 0;
|
|
let totalSize = 0;
|
|
$: progress = Math.floor((completed / totalSize) * 100).toString();
|
|
|
|
function xhrDownloadFileId(fileId: string) {
|
|
return new Promise<Blob>((resolve, reject) => {
|
|
if (!gapiLoaded || !gapi.auth.getToken()) {
|
|
reject(new Error('Not authenticated'));
|
|
return;
|
|
}
|
|
|
|
const { access_token } = gapi.auth.getToken();
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
completed = 0;
|
|
totalSize = 0;
|
|
|
|
xhr.open('GET', `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`);
|
|
xhr.setRequestHeader('Authorization', `Bearer ${access_token}`);
|
|
xhr.responseType = 'blob';
|
|
|
|
xhr.onprogress = ({ loaded, total }) => {
|
|
loadingMessage = '';
|
|
completed = loaded;
|
|
totalSize = total;
|
|
};
|
|
|
|
xhr.onabort = (event) => {
|
|
console.warn(`xhr ${fileId}: download aborted at ${event.loaded} of ${event.total}`);
|
|
showSnackbar('Download failed');
|
|
reject(new Error('Download aborted'));
|
|
};
|
|
|
|
xhr.onerror = (event) => {
|
|
console.error(`xhr ${fileId}: download error at ${event.loaded} of ${event.total}`);
|
|
showSnackbar('Download failed');
|
|
reject(new Error('Error downloading file'));
|
|
};
|
|
|
|
xhr.onload = () => {
|
|
completed = 0;
|
|
totalSize = 0;
|
|
resolve(xhr.response);
|
|
};
|
|
|
|
xhr.ontimeout = (event) => {
|
|
console.warn(`xhr ${fileId}: download timeout after ${event.loaded} of ${event.total}`);
|
|
showSnackbar('Download timed out');
|
|
reject(new Error('Timout downloading file'));
|
|
};
|
|
|
|
xhr.send();
|
|
});
|
|
}
|
|
|
|
async function connectDrive(resp?: any) {
|
|
if (resp?.error !== undefined) {
|
|
throw resp;
|
|
}
|
|
|
|
accessToken = resp?.access_token;
|
|
loadingMessage = 'Connecting to drive';
|
|
|
|
try {
|
|
const { result: readerFolderRes } = await gapi.client.drive.files.list({
|
|
q: `mimeType='application/vnd.google-apps.folder' and name='${READER_FOLDER}'`,
|
|
fields: 'files(id)'
|
|
});
|
|
|
|
if (readerFolderRes.files?.length === 0) {
|
|
const { result: createReaderFolderRes } = await gapi.client.drive.files.create({
|
|
resource: { mimeType: FOLDER_MIME_TYPE, name: READER_FOLDER },
|
|
fields: 'id'
|
|
});
|
|
|
|
readerFolderId = createReaderFolderRes.id || '';
|
|
} else {
|
|
const id = readerFolderRes.files?.[0]?.id || '';
|
|
readerFolderId = id || '';
|
|
}
|
|
|
|
const { result: volumeDataRes } = await gapi.client.drive.files.list({
|
|
q: `'${readerFolderId}' in parents and name='${VOLUME_DATA_FILE}'`,
|
|
fields: 'files(id, name)'
|
|
});
|
|
|
|
if (volumeDataRes.files?.length !== 0) {
|
|
volumeDataId = volumeDataRes.files?.[0].id || '';
|
|
}
|
|
|
|
const { result: profilesRes } = await gapi.client.drive.files.list({
|
|
q: `'${readerFolderId}' in parents and name='${PROFILES_FILE}'`,
|
|
fields: 'files(id, name)'
|
|
});
|
|
|
|
if (profilesRes.files?.length !== 0) {
|
|
profilesId = profilesRes.files?.[0].id || '';
|
|
}
|
|
|
|
loadingMessage = '';
|
|
errorMessage = '';
|
|
|
|
if (accessToken) {
|
|
showSnackbar('Connected to Google Drive');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error connecting to Drive:', error);
|
|
errorMessage = 'Failed to connect to Google Drive. Please check your credentials.';
|
|
loadingMessage = '';
|
|
}
|
|
}
|
|
|
|
function signIn() {
|
|
if (!gapiLoaded || !googleLoaded) {
|
|
errorMessage = 'Google APIs not loaded yet. Please wait and try again.';
|
|
return;
|
|
}
|
|
|
|
if (gapi.client.getToken() === null) {
|
|
tokenClient.requestAccessToken({ prompt: 'consent' });
|
|
} else {
|
|
tokenClient.requestAccessToken({ prompt: '' });
|
|
}
|
|
}
|
|
|
|
onMount(() => {
|
|
// Check if environment variables are set
|
|
if (!CLIENT_ID || !API_KEY) {
|
|
errorMessage = 'Google Drive integration not configured. Please set VITE_GDRIVE_CLIENT_ID and VITE_GDRIVE_API_KEY environment variables.';
|
|
return;
|
|
}
|
|
|
|
// Load Google APIs
|
|
if (typeof gapi !== 'undefined') {
|
|
gapiLoaded = true;
|
|
gapi.load('client', async () => {
|
|
try {
|
|
await gapi.client.init({
|
|
apiKey: API_KEY,
|
|
discoveryDocs: [DISCOVERY_DOC]
|
|
});
|
|
} catch (error) {
|
|
console.error('Error initializing gapi client:', error);
|
|
errorMessage = 'Failed to initialize Google Drive client.';
|
|
}
|
|
});
|
|
|
|
gapi.load('picker', () => {});
|
|
} else {
|
|
errorMessage = 'Google APIs not loaded. Please check your internet connection.';
|
|
}
|
|
|
|
if (typeof google !== 'undefined') {
|
|
googleLoaded = true;
|
|
tokenClient = google.accounts.oauth2.initTokenClient({
|
|
client_id: CLIENT_ID,
|
|
scope: SCOPES,
|
|
callback: connectDrive
|
|
});
|
|
} else {
|
|
errorMessage = 'Google OAuth not loaded. Please check your internet connection.';
|
|
}
|
|
});
|
|
|
|
function createPicker() {
|
|
if (!googleLoaded || !accessToken) {
|
|
showSnackbar('Not connected to Google Drive');
|
|
return;
|
|
}
|
|
|
|
const docsView = new google.picker.DocsView(google.picker.ViewId.DOCS)
|
|
.setMimeTypes('application/zip,application/x-zip-compressed')
|
|
.setMode(google.picker.DocsViewMode.LIST)
|
|
.setIncludeFolders(true)
|
|
.setParent(readerFolderId);
|
|
|
|
const picker = new google.picker.PickerBuilder()
|
|
.addView(docsView)
|
|
.setOAuthToken(accessToken)
|
|
.setAppId(CLIENT_ID)
|
|
.setDeveloperKey(API_KEY)
|
|
.enableFeature(google.picker.Feature.NAV_HIDDEN)
|
|
.setCallback(pickerCallback)
|
|
.build();
|
|
picker.setVisible(true);
|
|
}
|
|
|
|
async function pickerCallback(data: google.picker.ResponseObject) {
|
|
try {
|
|
if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
|
|
loadingMessage = 'Downloading from drive...';
|
|
const docs = data[google.picker.Response.DOCUMENTS];
|
|
const blob = await xhrDownloadFileId(docs[0].id);
|
|
|
|
loadingMessage = 'Adding to catalog...';
|
|
|
|
const file = new File([blob], docs[0].name);
|
|
|
|
await processFiles([file]);
|
|
loadingMessage = '';
|
|
}
|
|
} catch (error) {
|
|
showSnackbar('Something went wrong');
|
|
loadingMessage = '';
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
async function onUploadVolumeData() {
|
|
const metadata = {
|
|
mimeType: type,
|
|
name: VOLUME_DATA_FILE,
|
|
parents: [volumeDataId ? null : readerFolderId]
|
|
};
|
|
|
|
loadingMessage = 'Uploading volume data';
|
|
|
|
const res = await uploadFile({
|
|
accessToken,
|
|
fileId: volumeDataId,
|
|
metadata,
|
|
localStorageId: 'volumes',
|
|
type
|
|
});
|
|
|
|
volumeDataId = res.id;
|
|
loadingMessage = '';
|
|
|
|
if (volumeDataId) {
|
|
showSnackbar('Volume data uploaded');
|
|
}
|
|
}
|
|
|
|
async function onUploadProfiles() {
|
|
const metadata = {
|
|
mimeType: type,
|
|
name: PROFILES_FILE,
|
|
parents: [profilesId ? null : readerFolderId]
|
|
};
|
|
|
|
loadingMessage = 'Uploading profiles';
|
|
|
|
const res = await uploadFile({
|
|
accessToken,
|
|
fileId: profilesId,
|
|
metadata,
|
|
localStorageId: 'profiles',
|
|
type
|
|
});
|
|
|
|
profilesId = res.id;
|
|
loadingMessage = '';
|
|
|
|
if (profilesId) {
|
|
showSnackbar('Profiles uploaded');
|
|
}
|
|
}
|
|
|
|
async function onDownloadVolumeData() {
|
|
loadingMessage = 'Downloading volume data';
|
|
|
|
const { body } = await gapi.client.drive.files.get({
|
|
fileId: volumeDataId,
|
|
alt: 'media'
|
|
});
|
|
|
|
const downloaded = JSON.parse(body);
|
|
|
|
volumes.update((prev) => {
|
|
return {
|
|
...prev,
|
|
...downloaded
|
|
};
|
|
});
|
|
|
|
loadingMessage = '';
|
|
showSnackbar('Volume data downloaded');
|
|
}
|
|
|
|
async function onDownloadProfiles() {
|
|
loadingMessage = 'Downloading profiles';
|
|
|
|
const { body } = await gapi.client.drive.files.get({
|
|
fileId: profilesId,
|
|
alt: 'media'
|
|
});
|
|
|
|
const downloaded = JSON.parse(body);
|
|
|
|
profiles.update((prev) => {
|
|
return {
|
|
...prev,
|
|
...downloaded
|
|
};
|
|
});
|
|
|
|
loadingMessage = '';
|
|
showSnackbar('Profiles downloaded');
|
|
}
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Cloud</title>
|
|
</svelte:head>
|
|
|
|
<div class="p-2 h-[90svh]">
|
|
{#if errorMessage}
|
|
<div class="bg-red-900/20 border border-red-700 rounded-lg p-4 mb-4">
|
|
<p class="text-red-400">{errorMessage}</p>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if loadingMessage || completed > 0}
|
|
<Loader>
|
|
{#if completed > 0}
|
|
<P>{formatBytes(completed)} / {formatBytes(totalSize)}</P>
|
|
<Progressbar {progress} />
|
|
{:else}
|
|
{loadingMessage}
|
|
{/if}
|
|
</Loader>
|
|
{:else if accessToken}
|
|
<div class="flex justify-between items-center gap-6 flex-col">
|
|
<h2 class="text-3xl font-semibold text-center pt-2">Google Drive:</h2>
|
|
<p class="text-center">
|
|
Add your zipped manga files to the <span class="text-primary-700">{READER_FOLDER}</span> folder
|
|
in your Google Drive.
|
|
</p>
|
|
<div class="flex flex-col gap-4 w-full max-w-3xl">
|
|
<Button color="blue" on:click={createPicker}>Download manga</Button>
|
|
<div class="flex-col gap-2 flex">
|
|
<Button
|
|
color="dark"
|
|
on:click={() => promptConfirmation('Upload volume data?', onUploadVolumeData)}
|
|
>
|
|
Upload volume data
|
|
</Button>
|
|
{#if volumeDataId}
|
|
<Button
|
|
color="alternative"
|
|
on:click={() =>
|
|
promptConfirmation('Download and overwrite volume data?', onDownloadVolumeData)}
|
|
>
|
|
Download volume data
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
<div class="flex-col gap-2 flex">
|
|
<Button
|
|
color="dark"
|
|
on:click={() => promptConfirmation('Upload profiles?', onUploadProfiles)}
|
|
>
|
|
Upload profiles
|
|
</Button>
|
|
{#if profilesId}
|
|
<Button
|
|
color="alternative"
|
|
on:click={() =>
|
|
promptConfirmation('Download and overwrite profiles?', onDownloadProfiles)}
|
|
>
|
|
Download profiles
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<div class="flex justify-center pt-0 sm:pt-32">
|
|
<button
|
|
class="w-full border rounded-lg border-slate-600 p-10 border-opacity-50 hover:bg-slate-800 max-w-3xl"
|
|
on:click={signIn}
|
|
disabled={!gapiLoaded || !googleLoaded}
|
|
>
|
|
<div class="flex sm:flex-row flex-col gap-2 items-center justify-center">
|
|
<GoogleSolid size="lg" />
|
|
<h2 class="text-lg">Connect to Google Drive</h2>
|
|
</div>
|
|
</button>
|
|
{#if !gapiLoaded || !googleLoaded}
|
|
<p class="text-sm text-gray-400 mt-2">Loading Google APIs...</p>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|