Skip to content

Commit

Permalink
undo individual local changes
Browse files Browse the repository at this point in the history
  • Loading branch information
Richard Xiong committed Nov 16, 2024
1 parent 421410f commit 7e2d073
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 75 deletions.
2 changes: 1 addition & 1 deletion helpers/drive.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ky, { KyInstance } from "ky";
import ky from "ky";
import ObsidianGoogleDrive from "main";
import { getDriveKy } from "./ky";

Expand Down
40 changes: 6 additions & 34 deletions helpers/pull.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ObsidianGoogleDrive from "main";
import { Notice, TAbstractFile, TFile, TFolder } from "obsidian";
import { Notice, TFile, TFolder } from "obsidian";
import {
batchAsyncs,
FileMetadata,
Expand All @@ -21,34 +21,6 @@ export const pull = async (

const { vault } = t.app;

const createFolder = async (path: string) => {
const oldOperation = t.settings.operations[path];
await vault.createFolder(path);
t.settings.operations[path] = oldOperation;
if (!oldOperation) delete t.settings.operations[path];
};

const createFile = async (path: string, content: ArrayBuffer) => {
const oldOperation = t.settings.operations[path];
await vault.createBinary(path, content);
t.settings.operations[path] = oldOperation;
if (!oldOperation) delete t.settings.operations[path];
};

const modifyFile = async (file: TFile, content: ArrayBuffer) => {
const oldOperation = t.settings.operations[file.path];
await vault.modifyBinary(file, content);
t.settings.operations[file.path] = oldOperation;
if (!oldOperation) delete t.settings.operations[file.path];
};

const deleteFile = async (file: TAbstractFile) => {
const oldOperation = t.settings.operations[file.path];
await t.app.fileManager.trashFile(file);
delete t.settings.operations[file.path];
if (!oldOperation) delete t.settings.operations[file.path];
};

if (!t.accessToken.token) await refreshAccessToken(t);

const recentlyModified = await t.drive.searchFiles({
Expand Down Expand Up @@ -121,7 +93,7 @@ export const pull = async (
}
return;
}
return deleteFile(file);
return t.deleteFile(file);
})
);

Expand Down Expand Up @@ -152,7 +124,7 @@ export const pull = async (
});

for (const batch of batches) {
await Promise.all(batch.map((folder) => deleteFile(folder)));
await Promise.all(batch.map((folder) => t.deleteFile(folder)));
}
}
};
Expand Down Expand Up @@ -188,7 +160,7 @@ export const pull = async (
if (vault.getFolderByPath(folder.properties.path)) {
return;
}
return createFolder(folder.properties.path);
return t.createFolder(folder.properties.path);
})
);
}
Expand Down Expand Up @@ -223,10 +195,10 @@ export const pull = async (
);

if (localFile) {
return modifyFile(localFile, content);
return t.modifyFile(localFile, content);
}

createFile(file.properties.path, content);
return t.createFile(file.properties.path, content);
})
);
};
Expand Down
220 changes: 207 additions & 13 deletions helpers/push.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,78 @@
import ObsidianGoogleDrive from "main";
import { App, Modal, Notice, Setting, TFile, TFolder } from "obsidian";
import { batchAsyncs, getSyncMessage } from "./drive";
import { Modal, Notice, setIcon, Setting, TFile, TFolder } from "obsidian";
import { batchAsyncs, folderMimeType, getSyncMessage } from "./drive";
import { pull } from "./pull";

class ConfirmPushModal extends Modal {
proceed: (res: boolean) => void;

constructor(
app: App,
operations: [string, "create" | "delete" | "modify"][],
t: ObsidianGoogleDrive,
initialOperations: [string, "create" | "delete" | "modify"][],
proceed: (res: boolean) => void
) {
super(app);
super(t.app);
this.proceed = proceed;

this.setTitle("Push confirmation");
this.contentEl
.createEl("p")
.setText(
"Do you want to push the following changes to Google Drive:"
);
operations.forEach(([path, op]) => {
const p = this.contentEl.createEl("p");
p.createEl("b").setText(`${op[0].toUpperCase()}${op.slice(1)}`);
p.createSpan().setText(`: ${path}`);
});
this.proceed = proceed;
const container = this.contentEl.createEl("div");

const render = (operations: typeof initialOperations) => {
container.empty();
operations.map(([path, op]) => {
const div = container.createDiv();
div.addClass("operation-container");

const p = div.createEl("p");
p.createEl("b").setText(`${op[0].toUpperCase()}${op.slice(1)}`);
p.createSpan().setText(`: ${path}`);

if (
op === "delete" &&
operations.some(([file]) => path.startsWith(file + "/"))
) {
return;
}

const btn = div.createDiv().createEl("button");
setIcon(btn, "trash-2");
btn.onclick = async () => {
const nestedFiles = operations
.map(([file]) => file)
.filter(
(file) =>
file.startsWith(path + "/") || file === path
);
const proceed = await new Promise<boolean>((resolve) => {
new ConfirmUndoModal(
t,
op,
nestedFiles,
resolve
).open();
});

if (!proceed) return;

nestedFiles.forEach(
(file) => delete t.settings.operations[file]
);
const newOperations = operations.filter(
([file]) => !nestedFiles.includes(file)
);
if (!newOperations.length) return this.close();
render(newOperations);
};
});
};

render(initialOperations);

new Setting(this.contentEl)
.addButton((btn) =>
btn.setButtonText("Cancel").onClick(() => this.close())
Expand All @@ -44,15 +93,160 @@ class ConfirmPushModal extends Modal {
}
}

class ConfirmUndoModal extends Modal {
proceed: (res: boolean) => void;
t: ObsidianGoogleDrive;
filePathToId: Record<string, string>;

constructor(
t: ObsidianGoogleDrive,
operation: "create" | "delete" | "modify",
files: string[],
proceed: (res: boolean) => void
) {
super(t.app);
this.t = t;
this.filePathToId = Object.fromEntries(
Object.entries(this.t.settings.driveIdToPath).map(([id, path]) => [
path,
id,
])
);

const operationMap = {
create: "creating",
delete: "deleting",
modify: "modifying",
};

this.setTitle("Undo confirmation");
this.contentEl
.createEl("p")
.setText(
`Are you sure you want to undo ${operationMap[operation]} the following file(s):`
);
this.contentEl.createEl("ul").append(
...files.map((file) => {
const li = this.contentEl.createEl("li");
li.addClass("operation-file");
li.setText(file);
return li;
})
);
this.proceed = proceed;
new Setting(this.contentEl)
.addButton((btn) =>
btn.setButtonText("Cancel").onClick(() => this.close())
)
.addButton((btn) =>
btn
.setButtonText("Confirm")
.setCta()
.onClick(async () => {
btn.setDisabled(true);
if (operation === "delete") {
await this.handleDelete(files);
}
if (operation === "create") {
await this.handleCreate(files[0]);
}
if (operation === "modify") {
await this.handleModify(files[0]);
}
proceed(true);
this.close();
})
);
}

onClose() {
this.proceed(false);
}

async handleDelete(paths: string[]) {
const files = await this.t.drive.searchFiles({
include: ["id", "mimeType", "properties"],
matches: paths.map((path) => ({ properties: { path } })),
});
if (!files) {
return new Notice("An error occurred fetching Google Drive files.");
}

const filePathToMimeType = Object.fromEntries(
files.map((file) => [file.properties.path, file.mimeType])
);

const deletedFolders = paths.filter(
(path) => filePathToMimeType[path] === folderMimeType
);

if (deletedFolders.length) {
const batches: string[][] = new Array(
Math.max(
...deletedFolders.map((path) => path.split("/").length)
)
).fill([]);
deletedFolders.forEach((path) => {
batches[path.split("/").length - 1].push(path);
});

for (const batch of batches) {
await Promise.all(
batch.map((folder) => this.t.createFolder(folder))
);
}
}

const deletedFiles = paths.filter(
(path) => filePathToMimeType[path] !== folderMimeType
);

await batchAsyncs(
deletedFiles.map((path) => async () => {
const onlineFile = await this.t.drive
.getFile(this.filePathToId[path])
.arrayBuffer();
if (!onlineFile) {
return new Notice(
"An error occurred fetching Google Drive files."
);
}
return this.t.createFile(path, onlineFile);
})
);
}

async handleCreate(path: string) {
const file = this.app.vault.getAbstractFileByPath(path);
if (!file) return;
return this.t.deleteFile(file);
}

async handleModify(path: string) {
const file = this.app.vault.getFileByPath(path);
if (!file) return;

const onlineFile = await this.t.drive
.getFile(this.filePathToId[path])
.arrayBuffer();
if (!onlineFile) {
return new Notice("An error occurred fetching Google Drive files.");
}
return this.t.modifyFile(file, onlineFile);
}
}

export const push = async (t: ObsidianGoogleDrive) => {
if (t.syncing) return;
const initialOperations = Object.entries(t.settings.operations);
const initialOperations = Object.entries(t.settings.operations).sort(
([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)
); // Alphabetical
if (!initialOperations.length) {
return new Notice("No changes to push.");
}

const proceed = await new Promise<boolean>((resolve) => {
new ConfirmPushModal(t.app, initialOperations, resolve).open();
new ConfirmPushModal(t, initialOperations, resolve).open();
});

if (!proceed) return;
Expand Down
Loading

0 comments on commit 7e2d073

Please sign in to comment.