Add dialog to upload all files to GitHub

This commit is contained in:
Silvano Cerza 2025-01-11 15:41:03 +01:00
parent 9b5f974782
commit 81b7f2b235
4 changed files with 234 additions and 4 deletions

View file

@ -6,6 +6,7 @@ import MetadataStore from "./metadata-store";
import EventsListener from "./events/listener";
import EventsConsumer from "./events/consumer";
import { type Event } from "./events/types";
import { UploadDialog } from "./views/upload-all-files-dialog/view";
export default class GitHubSyncPlugin extends Plugin {
settings: GitHubSyncSettings;
@ -81,7 +82,7 @@ export default class GitHubSyncPlugin extends Plugin {
name: "Upload all files to GitHub",
repeatable: false,
icon: "arrow-up-from-line",
callback: async () => await this.uploadAllFiles(),
callback: async () => await this.openUploadAllFilesDialog(),
});
}
@ -135,8 +136,13 @@ export default class GitHubSyncPlugin extends Plugin {
this.updateStatusBarItem();
}
private async uploadAllFiles() {
// TODO
/**
* Opens dialog to upload all file in the tracked folder.
* This doesn't take into account the state of the files and uploads
* ALL files whether they've been modified or not.
*/
private async openUploadAllFilesDialog() {
new UploadDialog(this).open();
this.updateStatusBarItem();
}
@ -265,7 +271,7 @@ export default class GitHubSyncPlugin extends Plugin {
this.uploadAllFilesRibbonIcon = this.addRibbonIcon(
"arrow-up-from-line",
"Upload all files to GitHub",
async () => await this.uploadAllFiles(),
async () => await this.openUploadAllFilesDialog(),
);
}

10
src/views/hooks.ts Normal file
View file

@ -0,0 +1,10 @@
import { createContext, useContext } from "react";
import GitHubSyncPlugin from "src/main";
export const PluginContext = createContext<GitHubSyncPlugin | undefined>(
undefined,
);
export const usePlugin = (): GitHubSyncPlugin | undefined => {
return useContext(PluginContext);
};

View file

@ -0,0 +1,181 @@
import { usePlugin } from "../hooks";
import { useState, useMemo, useEffect } from "react";
const UploadDialogQuestion = ({
fileCount,
onCancel,
startUpload: onUpload,
}: {
fileCount: number;
onCancel: () => void;
startUpload: () => Promise<void>;
}) => {
return (
<div
style={{
display: "flex",
flexFlow: "column",
}}
>
<h1 style={{ width: "fit-content" }}>Upload files to GitHub</h1>
<p style={{ width: "fit-content" }}>
Do you want to upload{" "}
<span style={{ fontWeight: "bold" }}>{fileCount}</span> files? This
might take a while.
</p>
<div className="modal-button-container">
<button onClick={onCancel}>Cancel</button>
<button
onClick={async () => await onUpload()}
style={{
color: "var(--text-on-accent)",
backgroundColor: "var(--interactive-accent)",
}}
>
Upload
</button>
</div>
</div>
);
};
const UploadDialogUploading = ({
completedCount,
fileCount,
onCancel,
}: {
completedCount: number;
fileCount: number;
onCancel: () => void;
}) => {
const progress = (completedCount * 100) / fileCount;
return (
<div>
<h1 style={{ width: "fit-content" }}>
{progress === 100 ? "All files uploaded" : "Uploading…"}
</h1>
<div className="setting-progress-bar">
<div
className="setting-progress-bar-inner"
style={{
width: `${progress}%`,
}}
/>
</div>
<p>
Uploaded {completedCount} file{completedCount === 1 ? "" : "s"} of{" "}
{fileCount}
</p>
<div className="modal-button-container">
{/*
We use onCancel for both cases as it won't have any effect other
than closing the modal if upload is done
*/}
<button onClick={onCancel}>{progress === 100 ? "Ok" : "Cancel"}</button>
</div>
</div>
);
};
const UploadDialogContent = ({ onCancel }: { onCancel: () => void }) => {
const plugin = usePlugin();
if (!plugin) {
// Unlikely to happen, makes TS happy though
throw new Error("Plugin is not initialized");
}
const [uploading, setUploading] = useState(false);
const [completedCount, setCompletedCount] = useState(0);
const files = useMemo(
// This is not the most efficient way to get all files in the
// folder to sync but it's quick to code.
// I'll enhance it in the future.
() =>
plugin?.app.vault
.getFiles()
.filter((file) =>
file.path.startsWith(plugin.settings.localContentDir),
),
[],
);
const abortController = useMemo(() => new AbortController(), []);
// Stop the upload when the component is unmounted, otherwise
// we keep uploading even when the modal is closed.
useEffect(() => {
return () => {
abortController.abort();
};
}, []);
const startUploading = async () => {
setUploading(true);
// We upload files sequentially to avoid conflicts.
// GitHub rejects commits if they're made in fast succession, thus
// forcing us to retry the failed upload.
// So parallelization is not an option.
for (const file of files) {
console.log(`Uploading ${file.path}`);
if (abortController.signal.aborted) {
return;
}
await plugin.client.uploadFile(
plugin.settings.githubOwner,
plugin.settings.githubRepo,
plugin.settings.githubBranch,
file.path,
);
// Reset dirty state
plugin.metadataStore.data[file.path].dirty = false;
// Gets the new SHA of the file
const sha = await plugin.client.getFileSha(
plugin.settings.githubOwner,
plugin.settings.githubRepo,
plugin.settings.githubBranch,
file.path,
);
plugin.metadataStore.data[file.path].sha = sha;
await plugin.metadataStore.save();
if (abortController.signal.aborted) {
// Check abort state only after the uploaded file metadata
// is updated, otherwise we risk having outdated SHAs
return;
}
setCompletedCount((count) => count + 1);
console.log(`Uploaded ${file.path}`);
}
};
return (
<div
style={{
display: "flex",
flexFlow: "column",
}}
>
{!uploading && (
<UploadDialogQuestion
fileCount={files.length}
onCancel={onCancel}
startUpload={startUploading}
/>
)}
{uploading && (
<UploadDialogUploading
completedCount={completedCount}
fileCount={files.length}
onCancel={() => {
abortController.abort();
onCancel();
}}
/>
)}
</div>
);
};
export default UploadDialogContent;

View file

@ -0,0 +1,33 @@
import { Modal } from "obsidian";
import { Root, createRoot } from "react-dom/client";
import GitHubSyncPlugin from "src/main";
import { PluginContext } from "../hooks";
import UploadDialogContent from "./component";
/**
* Dialog shown when user tries to upload all local files to GitHub.
*/
export class UploadDialog extends Modal {
root: Root | null = null;
constructor(private plugin: GitHubSyncPlugin) {
super(plugin.app);
}
onOpen() {
this.root = createRoot(this.modalEl);
this.root.render(
<PluginContext.Provider value={this.plugin}>
<UploadDialogContent onCancel={() => this.close()} />
</PluginContext.Provider>,
);
}
onClose() {
const { contentEl } = this;
// It's important to unmount the component so upload stops
// in case it's running
this.root?.unmount();
contentEl.empty();
}
}