google drive is usable now

This commit is contained in:
fyears
2024-06-02 23:37:53 +08:00
parent 2ace90155c
commit e116bb1deb
35 changed files with 1595 additions and 24 deletions
+6 -2
View File
@@ -4,9 +4,9 @@
Remotely Save has some "pro features", which users have to pay for using them.
## Sign Up / Sign In
## Sign Up / Sign In And Connect
Please go to <https://remotelysave.com> to sign up and sign in an account firstly.
See the tutorial about your PRO account [here](../docs/pro/README.md).
## Smart Conflict
@@ -14,6 +14,10 @@ Basic (free) version can detect conflicts, but users have to choose to keep newe
PRO (paid) feature "Smart Conflict" gives users one more option: merge small markdown files, or duplicate large markdown files or non-markdown files.
## Sync With Google Drive
PRO (paid) feature "sync with Google Drive" allows users to to sync with Google Drive. Tutorials and limitations are documented [here](../docs/remote_services/googledrive/README.md).
## License
The codes or files or subfolders inside the current folder (`pro` in the repo), are released under "source available" license: "PolyForm Strict License 1.0.0".
+36 -10
View File
@@ -246,13 +246,7 @@ export const checkProRunnableAndFixInplace = async (
pluginVersion: string,
saveUpdatedConfigFunc: () => Promise<any> | undefined
): Promise<true> => {
// if no pro features are used, we are good to go, no checking
if (
featuresToCheck.contains("feature-smart_conflict") &&
config.conflictAction !== "smart_conflict"
) {
return true;
}
console.debug(`checkProRunnableAndFixInplace`);
// many checks if status is valid
@@ -280,6 +274,8 @@ export const checkProRunnableAndFixInplace = async (
}
}
const errorMsgs = [];
// check for the features
if (featuresToCheck.contains("feature-smart_conflict")) {
if (config.conflictAction === "smart_conflict") {
@@ -288,15 +284,45 @@ export const checkProRunnableAndFixInplace = async (
(x) => x.featureName === "feature-smart_conflict"
).length === 1
) {
return true;
// good to go
} else {
throw Error(
errorMsgs.push(
`You're trying to use "smart conflict" PRO feature but you haven't subscribe to it.`
);
}
} else {
return true;
// good to go
}
}
if (featuresToCheck.contains("feature-google_drive")) {
console.debug(
`checking "feature-google_drive", serviceType=${config.serviceType}`
);
console.debug(
`enabledProFeatures=${JSON.stringify(config.pro.enabledProFeatures)}`
);
if (config.serviceType === "googledrive") {
if (
config.pro.enabledProFeatures.filter(
(x) => x.featureName === "feature-google_drive"
).length === 1
) {
// good to go
} else {
errorMsgs.push(
`You're trying to use "sync with Google Drive" PRO feature but you haven't subscribe to it.`
);
}
} else {
// good to go
}
}
if (errorMsgs.length !== 0) {
throw Error(errorMsgs.join("\n\n"));
}
return true;
};
+15
View File
@@ -23,3 +23,18 @@ export interface ProConfig {
enabledProFeatures: FeatureInfo[];
credentialsShouldBeDeletedAtTimeMs?: number;
}
export interface GoogleDriveConfig {
accessToken: string;
accessTokenExpiresInMs: number;
accessTokenExpiresAtTimeMs: number;
refreshToken: string;
remoteBaseDir?: string;
credentialsShouldBeDeletedAtTimeMs?: number;
scope: "https://www.googleapis.com/auth/drive.file";
}
export const DEFAULT_GOOGLEDRIVE_CLIENT_ID =
process.env.DEFAULT_GOOGLEDRIVE_CLIENT_ID;
export const DEFAULT_GOOGLEDRIVE_CLIENT_SECRET =
process.env.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET;
+765
View File
@@ -0,0 +1,765 @@
// https://developers.google.com/identity/protocols/oauth2/native-app
// https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow
// https://developers.google.com/identity/protocols/oauth2/web-server
import { entries } from "lodash";
import * as mime from "mime-types";
import { requestUrl } from "obsidian";
import PQueue from "p-queue";
import { DEFAULT_CONTENT_TYPE, type Entity } from "../../src/baseTypes";
import { FakeFs } from "../../src/fsAll";
import {
getFolderLevels,
splitFileSizeToChunkRanges,
unixTimeToStr,
} from "../../src/misc";
import {
DEFAULT_GOOGLEDRIVE_CLIENT_ID,
DEFAULT_GOOGLEDRIVE_CLIENT_SECRET,
type GoogleDriveConfig,
} from "./baseTypesPro";
export const DEFAULT_GOOGLEDRIVE_CONFIG: GoogleDriveConfig = {
accessToken: "",
refreshToken: "",
accessTokenExpiresInMs: 0,
accessTokenExpiresAtTimeMs: 0,
credentialsShouldBeDeletedAtTimeMs: 0,
scope: "https://www.googleapis.com/auth/drive.file",
};
const FOLDER_MIME_TYPE = "application/vnd.google-apps.folder";
/**
* A simplified version of the type
*
*/
interface File {
kind?: string;
driveId?: string;
fileExtension?: string;
copyRequiresWriterPermission?: boolean;
md5Checksum?: string;
writersCanShare?: boolean;
viewedByMe?: boolean;
mimeType?: string;
parents?: string[];
thumbnailLink?: string;
iconLink?: string;
shared?: boolean;
headRevisionId?: string;
webViewLink?: string;
webContentLink?: string;
size?: string;
viewersCanCopyContent?: boolean;
hasThumbnail?: boolean;
spaces?: string[];
folderColorRgb?: string;
id?: string;
name?: string;
description?: string;
starred?: boolean;
trashed?: boolean;
explicitlyTrashed?: boolean;
createdTime?: string;
modifiedTime?: string;
modifiedByMeTime?: string;
viewedByMeTime?: string;
sharedWithMeTime?: string;
quotaBytesUsed?: string;
version?: string;
originalFilename?: string;
ownedByMe?: boolean;
fullFileExtension?: string;
isAppAuthorized?: boolean;
teamDriveId?: string;
hasAugmentedPermissions?: boolean;
thumbnailVersion?: string;
trashedTime?: string;
modifiedByMe?: boolean;
permissionIds?: string[];
resourceKey?: string;
sha1Checksum?: string;
sha256Checksum?: string;
}
interface GDEntity extends Entity {
id: string;
parentID: string | undefined;
parentIDPath: string | undefined;
isFolder: boolean;
}
/**
* https://developers.google.com/identity/protocols/oauth2/web-server#httprest_7
* @param refreshToken
*/
export const sendRefreshTokenReq = async (refreshToken: string) => {
console.debug(`refreshing token`);
const x = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: DEFAULT_GOOGLEDRIVE_CLIENT_ID ?? "",
client_secret: DEFAULT_GOOGLEDRIVE_CLIENT_SECRET ?? "",
grant_type: "refresh_token",
refresh_token: refreshToken,
}).toString(),
});
if (x.status === 200) {
const y = await x.json();
console.debug(`new token obtained`);
return y;
} else {
throw Error(`cannot refresh an access token`);
}
// {
// "access_token": "1/fFAGRNJru1FTz70BzhT3Zg",
// "expires_in": 3920,
// "scope": "https://www.googleapis.com/auth/drive.file",
// "token_type": "Bearer"
// }
};
const fromFileToGDEntity = (
file: File,
parentID: string,
parentFolderPath: string | undefined /* for bfs */
) => {
if (parentID === undefined || parentID === "" || parentID === "root") {
throw Error(`parentID=${parentID} should not be in fromFileToGDEntity`);
}
let keyRaw = file.name!;
if (
parentFolderPath !== undefined &&
parentFolderPath !== "" &&
parentFolderPath !== "/"
) {
if (!parentFolderPath.endsWith("/")) {
throw Error(
`parentFolderPath=${parentFolderPath} should not be in fromFileToGDEntity`
);
}
keyRaw = `${parentFolderPath}${file.name}`;
}
const isFolder = file.mimeType === FOLDER_MIME_TYPE;
if (isFolder) {
keyRaw = `${keyRaw}/`;
}
return {
key: keyRaw,
keyRaw: keyRaw,
mtimeCli: Date.parse(file.modifiedTime!),
mtimeSvr: Date.parse(file.modifiedTime!),
size: isFolder ? 0 : Number.parseInt(file.size!),
sizeRaw: isFolder ? 0 : Number.parseInt(file.size!),
hash: isFolder ? undefined : file.md5Checksum!,
id: file.id!,
parentID: parentID,
isFolder: isFolder,
} as GDEntity;
};
export class FakeFsGoogleDrive extends FakeFs {
kind: string;
googleDriveConfig: GoogleDriveConfig;
remoteBaseDir: string;
vaultFolderExists: boolean;
saveUpdatedConfigFunc: () => Promise<any>;
keyToGDEntity: Record<string, GDEntity>;
baseDirID: string;
constructor(
googleDriveConfig: GoogleDriveConfig,
vaultName: string,
saveUpdatedConfigFunc: () => Promise<any>
) {
super();
this.kind = "googledrive";
this.googleDriveConfig = googleDriveConfig;
this.remoteBaseDir =
this.googleDriveConfig.remoteBaseDir || vaultName || "";
this.vaultFolderExists = false;
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
this.keyToGDEntity = {};
this.baseDirID = "";
}
async _init() {
// get accessToken
await this._getAccessToken();
// check vault folder exists
if (this.vaultFolderExists) {
// pass
} else {
const q = encodeURIComponent(
`name='${this.remoteBaseDir}' and mimeType='application/vnd.google-apps.folder' and trashed=false`
);
const url: string = `https://www.googleapis.com/drive/v3/files?q=${q}&pageSize=1000&fields=kind,nextPageToken,files(kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum)`;
const k = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${await this._getAccessToken()}`,
},
});
const k1: { files: File[] } = await k.json();
// console.debug(k1);
if (k1.files.length > 0) {
// yeah we find it
this.baseDirID = k1.files[0].id!;
this.vaultFolderExists = true;
} else {
// wait, we need to create the folder!
console.debug(`we need to create the base dir ${this.remoteBaseDir}`);
const meta: any = {
mimeType: FOLDER_MIME_TYPE,
name: this.remoteBaseDir,
};
const res = await fetch("https://www.googleapis.com/drive/v3/files", {
method: "POST",
headers: {
Authorization: `Bearer ${await this._getAccessToken()}`,
"Content-Type": "application/json",
},
body: JSON.stringify(meta),
});
const res2: File = await res.json();
if (res.status === 200) {
console.debug(`succeed to create the base dir ${this.remoteBaseDir}`);
this.baseDirID = res2.id!;
this.vaultFolderExists = true;
} else {
throw Error(
`cannot create base dir ${this.remoteBaseDir} in init func.`
);
}
}
}
}
async _getAccessToken() {
if (
this.googleDriveConfig.accessToken === "" ||
this.googleDriveConfig.refreshToken === ""
) {
throw Error("The user has not manually auth yet.");
}
const ts = Date.now();
if (this.googleDriveConfig.accessTokenExpiresAtTimeMs > ts) {
return this.googleDriveConfig.accessToken;
}
// refresh
const k = await sendRefreshTokenReq(this.googleDriveConfig.refreshToken);
this.googleDriveConfig.accessToken = k.access_token;
this.googleDriveConfig.accessTokenExpiresInMs = k.expires_in * 1000;
this.googleDriveConfig.accessTokenExpiresAtTimeMs =
ts + k.expires_in * 1000 - 60 * 2 * 1000;
await this.saveUpdatedConfigFunc();
console.info("Google Drive accessToken updated");
return this.googleDriveConfig.accessToken;
}
/**
* https://developers.google.com/drive/api/reference/rest/v3/files/list
*/
async walk(): Promise<Entity[]> {
await this._init();
const allFiles: GDEntity[] = [];
// bfs
const queue = new PQueue({
concurrency: 5, // TODO: make it configurable?
autoStart: true,
});
queue.on("error", (error) => {
queue.pause();
queue.clear();
throw error;
});
let parents = [
{
id: this.baseDirID, // special init, from already created root folder ID
folderPath: "",
},
];
while (parents.length !== 0) {
const children: typeof parents = [];
for (const { id, folderPath } of parents) {
queue.add(async () => {
const filesUnderFolder = await this._walkFolder(id, folderPath);
for (const f of filesUnderFolder) {
allFiles.push(f);
if (f.isFolder) {
// keyRaw itself already has a tailing slash, no more slash here
// keyRaw itself also already has full path
const child = {
id: f.id,
folderPath: f.keyRaw,
};
// console.debug(
// `looping result of _walkFolder(${id},${folderPath}), adding child=${JSON.stringify(
// child
// )}`
// );
children.push(child);
}
}
});
}
await queue.onIdle();
parents = children;
}
// console.debug(`in the end of walk:`);
// console.debug(allFiles);
// console.debug(this.keyToGDEntity);
return allFiles;
}
async _walkFolder(parentID: string, parentFolderPath: string) {
// console.debug(
// `input of single level: parentID=${parentID}, parentFolderPath=${parentFolderPath}`
// );
const filesOneLevel: GDEntity[] = [];
let nextPageToken: string | undefined = undefined;
if (parentID === undefined || parentID === "" || parentID === "root") {
// we should never start from root
// because we encapsulate the vault inside a folder
throw Error(`something goes wrong walking folder`);
}
do {
const q = encodeURIComponent(
`'${parentID}' in parents and trashed=false`
);
const pageToken =
nextPageToken !== undefined ? `&pageToken=${nextPageToken}` : "";
const url: string = `https://www.googleapis.com/drive/v3/files?q=${q}&pageSize=1000&fields=kind,nextPageToken,files(kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum)${pageToken}`;
const k = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${await this._getAccessToken()}`,
},
});
if (k.status !== 200) {
throw Error(`cannot walk for parentID=${parentID}`);
}
const k1 = await k.json();
// console.debug(k1);
for (const file of k1.files as File[]) {
const entity = fromFileToGDEntity(file, parentID, parentFolderPath);
this.keyToGDEntity[entity.keyRaw] = entity; // build cache
filesOneLevel.push(entity);
}
nextPageToken = k1.nextPageToken;
} while (nextPageToken !== undefined);
// console.debug(filesOneLevel);
return filesOneLevel;
}
async walkPartial(): Promise<Entity[]> {
await this._init();
const filesInLevel = await this._walkFolder(this.baseDirID, "");
return filesInLevel;
}
/**
* https://developers.google.com/drive/api/reference/rest/v3/files/get
* https://developers.google.com/drive/api/guides/fields-parameter
*/
async stat(key: string): Promise<Entity> {
await this._init();
// TODO: we already have a cache, should we call again?
const cachedEntity = this.keyToGDEntity[key];
const fileID = cachedEntity?.id;
if (cachedEntity === undefined || fileID === undefined) {
throw Error(`no fileID found for key=${key}`);
}
const url: string = `https://www.googleapis.com/drive/v3/files/${fileID}?fields=kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum`;
const k = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${await this._getAccessToken()}`,
},
});
if (k.status !== 200) {
throw Error(`cannot get file meta fileID=${fileID}, key=${key}`);
}
const k1: File = await k.json();
const entity = fromFileToGDEntity(
k1,
cachedEntity.parentID!,
cachedEntity.parentIDPath!
);
// insert back to cache?? to update it??
this.keyToGDEntity[key] = entity;
return entity;
}
/**
* https://developers.google.com/drive/api/guides/folder
*/
async mkdir(
key: string,
mtime: number | undefined,
ctime: number | undefined
): Promise<Entity> {
if (!key.endsWith("/")) {
throw Error(`you should not mkdir on key=${key}`);
}
await this._init();
// xxx/ => ["xxx"]
// xxx/yyy/zzz/ => ["xxx", "xxx/yyy", "xxx/yyy/zzz"]
const folderLevels = getFolderLevels(key);
let parentFolderPath: string | undefined = undefined;
let parentID: string | undefined = undefined;
if (folderLevels.length === 0) {
throw Error(`cannot getFolderLevels of ${key}`);
} else if (folderLevels.length === 1) {
parentID = this.baseDirID;
parentFolderPath = ""; // ignore base dir
} else {
// length > 1
parentFolderPath = `${folderLevels[folderLevels.length - 2]}/`;
if (!(parentFolderPath in this.keyToGDEntity)) {
throw Error(
`parent of ${key}: ${parentFolderPath} is not created before??`
);
}
parentID = this.keyToGDEntity[parentFolderPath].id;
}
// xxx/yyy/zzz/ => ["xxx", "xxx/yyy", "xxx/yyy/zzz"] => "xxx/yyy/zzz" => "zzz"
let folderItselfWithoutSlash = folderLevels[folderLevels.length - 1];
folderItselfWithoutSlash = folderItselfWithoutSlash.split("/").pop()!;
const meta: any = {
mimeType: FOLDER_MIME_TYPE,
modifiedTime: unixTimeToStr(mtime, true),
createdTime: unixTimeToStr(ctime, true),
name: folderItselfWithoutSlash,
parents: [parentID],
};
const res = await fetch("https://www.googleapis.com/drive/v3/files", {
method: "POST",
headers: {
Authorization: `Bearer ${await this._getAccessToken()}`,
"Content-Type": "application/json",
},
body: JSON.stringify(meta),
});
if (res.status !== 200 && res.status !== 201) {
throw Error(`create folder ${key} failed! meta=${JSON.stringify(meta)}`);
}
const res2: File = await res.json();
// console.debug(res2);
const entity = fromFileToGDEntity(res2, parentID, parentFolderPath);
// insert into cache
this.keyToGDEntity[key] = entity;
return entity;
}
/**
* https://developers.google.com/drive/api/guides/manage-uploads
* https://stackoverflow.com/questions/65181932/how-i-can-upload-file-to-google-drive-with-google-drive-api
*/
async writeFile(
key: string,
content: ArrayBuffer,
mtime: number,
ctime: number
): Promise<Entity> {
if (key.endsWith("/")) {
throw Error(`should not call writeFile on ${key}`);
}
await this._init();
const contentType =
mime.contentType(mime.lookup(key) || DEFAULT_CONTENT_TYPE) ||
DEFAULT_CONTENT_TYPE;
let parentID: string | undefined = undefined;
let parentFolderPath: string | undefined = undefined;
// "xxx" => []
// "xxx/yyy/zzz.md" => ["xxx", "xxx/yyy"]
const folderLevels = getFolderLevels(key);
if (folderLevels.length === 0) {
// root
parentID = this.baseDirID;
parentFolderPath = "";
} else {
parentFolderPath = `${folderLevels[folderLevels.length - 1]}/`;
if (!(parentFolderPath in this.keyToGDEntity)) {
throw Error(
`parent of ${key}: ${parentFolderPath} is not created before??`
);
}
parentID = this.keyToGDEntity[parentFolderPath].id;
}
const fileItself = key.split("/").pop()!;
if (content.byteLength <= 5 * 1024 * 1024) {
const formData = new FormData();
const meta: any = {
name: fileItself,
modifiedTime: unixTimeToStr(mtime, true),
createdTime: unixTimeToStr(ctime, true),
parents: [parentID],
};
formData.append(
"metadata",
new Blob([JSON.stringify(meta)], {
type: "application/json; charset=UTF-8",
})
);
formData.append("media", new Blob([content], { type: contentType }));
const res = await fetch(
"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum",
{
method: "POST",
headers: {
Authorization: `Bearer ${await this._getAccessToken()}`,
},
body: formData,
}
);
if (res.status !== 200 && res.status !== 201) {
throw Error(`create file ${key} failed! meta=${JSON.stringify(meta)}`);
}
const res2: File = await res.json();
console.debug(
`upload ${key} with ${JSON.stringify(meta)}, res2=${JSON.stringify(
res2
)}`
);
const entity = fromFileToGDEntity(res2, parentID, parentFolderPath);
// insert into cache
this.keyToGDEntity[key] = entity;
return entity;
} else {
const meta: any = {
name: fileItself,
modifiedTime: unixTimeToStr(mtime, true),
createdTime: unixTimeToStr(ctime, true),
parents: [parentID],
};
const bodyStr = JSON.stringify(meta);
const headers: HeadersInit = {
Authorization: `Bearer ${await this._getAccessToken()}`,
"Content-Type": "application/json",
"Content-Length": `${bodyStr.length}`,
"X-Upload-Content-Type": contentType,
"X-Upload-Content-Length": `${content.byteLength}`,
};
const res = await fetch(
"https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&fields=kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum",
{
method: "POST",
headers: headers,
body: bodyStr,
}
);
if (res.status !== 200) {
throw Error(
`create resumable file ${key} failed! meta=${JSON.stringify(
meta
)}, header=${JSON.stringify(headers)}`
);
}
const uploadLocation = res.headers.get("Location");
if (uploadLocation === null || !uploadLocation.startsWith("http")) {
throw Error(
`create resumable file ${key} failed! meta=${JSON.stringify(
meta
)}, header=${JSON.stringify(headers)}`
);
}
console.debug(`key=${key}, uploadLocaltion=${uploadLocation}`);
// multiples of 256 KB (256 x 1024 bytes) in size
const sizePerChunk = 5 * 4 * 256 * 1024; // 5.24 mb
const chunkRanges = splitFileSizeToChunkRanges(
content.byteLength,
sizePerChunk
);
let entity: GDEntity | undefined = undefined;
// TODO: deal with "Resume an interrupted upload"
// currently (202405) only assume everything goes well...
// TODO: parallel
for (const { start, end } of chunkRanges) {
console.debug(
`key=${key}, start upload chunk ${start}-${end}/${content.byteLength}`
);
const res = await fetch(uploadLocation, {
method: "PUT",
headers: {
Authorization: `Bearer ${await this._getAccessToken()}`,
"Content-Length": `${end - start + 1}`, // the number of bytes in the current chunk
"Content-Range": `bytes ${start}-${end}/${content.byteLength}`,
},
body: content.slice(start, end + 1), // TODO: slice() is a copy, may be we can optimize it
});
if (res.status >= 400 && res.status <= 599) {
throw Error(
`create resumable file ${key} failed! meta=${JSON.stringify(
meta
)}, header=${JSON.stringify(headers)}`
);
}
if (res.status === 200 || res.status === 201) {
const res2: File = await res.json();
console.debug(
`upload ${key} with ${JSON.stringify(meta)}, res2=${JSON.stringify(
res2
)}`
);
if (res2.id === undefined || res2.id === null || res2.id === "") {
// TODO: what's this??
} else {
entity = fromFileToGDEntity(res2, parentID, parentFolderPath);
// insert into cache
this.keyToGDEntity[key] = entity;
}
}
}
if (entity === undefined) {
throw Error(`something goes wrong while uploading large file ${key}`);
}
return entity;
}
}
/**
* https://developers.google.com/drive/api/reference/rest/v3/files/get
*/
async readFile(key: string): Promise<ArrayBuffer> {
if (key.endsWith("/")) {
throw Error(`you should not call readFile on ${key}`);
}
await this._init();
const fileID = this.keyToGDEntity[key]?.id;
if (fileID === undefined) {
throw Error(`no fileID found for key=${key}`);
}
const res1 = await fetch(
`https://www.googleapis.com/drive/v3/files/${fileID}?alt=media`,
{
method: "GET",
headers: {
Authorization: `Bearer ${await this._getAccessToken()}`,
},
}
);
if (res1.status !== 200) {
throw Error(`cannot download ${key} using fileID=${fileID}`);
}
const res2 = await res1.arrayBuffer();
return res2;
}
async rename(key1: string, key2: string): Promise<void> {
throw new Error("Method not implemented.");
}
/**
* https://developers.google.com/drive/api/guides/delete
* https://developers.google.com/drive/api/reference/rest/v3/files/update
*/
async rm(key: string): Promise<void> {
await this._init();
const fileID = this.keyToGDEntity[key]?.id;
if (fileID === undefined) {
throw Error(`no fileID found for key=${key}`);
}
const res1 = await fetch(
`https://www.googleapis.com/drive/v3/files/${fileID}`,
{
method: "PATCH",
headers: {
Authorization: `Bearer ${await this._getAccessToken()}`,
},
body: JSON.stringify({
trashed: true,
}),
}
);
if (res1.status !== 200) {
throw Error(`cannot rm ${key} using fileID=${fileID}`);
}
}
async checkConnect(callbackFunc?: any): Promise<boolean> {
// if we can init, we can connect
try {
await this._init();
return true;
} catch (err) {
console.debug(err);
callbackFunc?.(err);
return false;
}
}
async getUserDisplayName(): Promise<string> {
throw new Error("Method not implemented.");
}
/**
* https://developers.google.com/identity/protocols/oauth2/web-server#tokenrevoke
*/
async revokeAuth(): Promise<any> {
const x = await fetch(
`https://oauth2.googleapis.com/revoke?token=${this._getAccessToken()}`,
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
if (x.status === 200) {
return true;
} else {
throw Error(`cannot revoke`);
}
}
allowEmptyFile(): boolean {
return true;
}
}
+36
View File
@@ -7,6 +7,22 @@
"protocol_pro_connect_fail": "Something went wrong from response from Remotely Save official website. Maybe the network connection is not good. Maybe you rejected the auth?",
"protocol_pro_connect_succ_revoke": "You've connected as user {{email}}. If you want to disconnect, click this button.",
"modal_googledriveauth_tutorial": "<p>Please firstly go to the address, then go on the auth flow. In the end, you will see a code, please paste that code here and submit.</p>",
"modal_googledriveauth_copybutton": "Click to copy the auth url",
"modal_googledriveauth_copynotice": "The auth url is copied to the clipboard!",
"modal_googledrivce_maualinput": "The Code from the website",
"modal_googledrivce_maualinput_desc": "Please input the code here from the end of auth flow, and press confirm.",
"modal_googledrive_maualinput_notice": "We are trying to connect to Google and update the credentials...",
"modal_googledrive_maualinput_succ_notice": "Great! The credentials are updated!",
"modal_googledrive_maualinput_fail_notice": "Oops! Failed to update the credentials. Please try again later.",
"modal_googledriverevokeauth_step1": "Step 1: Go to the following address, you can remove the connection there.",
"modal_googledriverevokeauth_step2": "Step 2: Click the button below, to clean the locally-saved login credentials.",
"modal_googledriverevokeauth_clean": "Clean Locally-Saved Login Credentials",
"modal_googledriverevokeauth_clean_desc": "You need to click the button.",
"modal_googledriverevokeauth_clean_button": "Clean",
"modal_googledriverevokeauth_clean_notice": "Cleaned!",
"modal_googledriverevokeauth_clean_fail": "Something goes wrong while revoking.",
"modal_prorevokeauth": "Revoke auth by clicking here and follow the steps.",
"modal_prorevokeauth_clean": "Clean",
"modal_prorevokeauth_clean_desc": "Clean local auth record",
@@ -20,6 +36,26 @@
"modal_proauth_maualinput_notice": "Trying to connect, wait...",
"modal_proauth_maualinput_conn_fail": "Failed to connect",
"settings_googledrive": "Google Drive (PRO) (beta)",
"settings_chooseservice_googledrive": "Google Drive (PRO) (beta)",
"settings_googledrive_disclaimer1": "Disclaimer: This app is NOT an official Google product. The app just uses Google Drive's public api.",
"settings_googledrive_disclaimer2": "Disclaimer: The information is stored locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Google Drive, please immediately disconnect this app on https://myaccount.google.com/permissions .",
"settings_googledrive_pro_desc": "<p><strong>!!It's a PRO feature of Remotely Save! You need a Remotely Save online account for this feature!!</strong>(<a href=\"#settings-pro\">scroll down</a> for more info about PRO account.)</p>",
"settings_googledrive_notshowuphint": "Google Drive Settings Not Available",
"settings_googledrive_notshowuphint_desc": "Google Drive settings are not available, because you haven't subscribed to the PRO feature in your Remotely Save account.",
"settings_googledrive_notshowuphint_view_pro": "View PRO Settings",
"settings_googledrive_folder": "We will create and sync inside the folder {{remoteBaseDir}} on your Google Drive. DO NOT create this folder by yourself manually.",
"settings_googledrive_revoke": "Revoke Auth",
"settings_googledrive_revoke_desc": "You've connected. If you want to disconnect, click this button.",
"settings_googledrive_revoke_button": "Revoke Auth",
"settings_googledrive_auth": "Auth",
"settings_googledrive_auth_desc": "Auth.",
"settings_googledrive_auth_button": "Auth",
"settings_googledrive_connect_succ": "Great! We can connect to Google Drive!",
"settings_googledrive_connect_fail": "We cannot connect to Google Drive.",
"settings_export_googledrive_button": "Export Google Drive Part",
"settings_pro": "Account (for PRO features)",
"settings_pro_tutorial": "<p>Using <stong>basic</strong> features of Remotely Save is <strong>FREE</strong> and do <strong>NOT</strong> need an account.</p><p>However, you will <strong>need</strong> an online account and <strong>PAY</strong> for the <strong>PRO</strong> features such as smart conflict.</p><p>Firstly please click the button to sign up and sign in to the website: <a href=\"https://remotelysave.com\">https://remotelysave.com</a>. Notice: It's different from, and NOT affiliated with Obsidian account.</p><p>Secondly please \"connect\" your local device to your online account.",
"settings_pro_features": "Features",
+36
View File
@@ -7,6 +7,22 @@
"protocol_pro_connect_fail": "Remotely Save 官网返回错误。可能是网络连接不稳定。也可能是您拒绝了授权?",
"protocol_pro_connect_succ_revoke": "您已连接上账号 {{email}}。如果要取消连接,请点击此按钮。",
"modal_googledriveauth_tutorial": "<p>请访问此网址,然后会进入授权流程。最后,您会看到一个码,请复制粘贴到这里然后提交。</p>",
"modal_googledriveauth_copybutton": "点击以复制网址",
"modal_googledriveauth_copynotice": "网址已复制!",
"modal_googledrivce_maualinput": "网站上的码",
"modal_googledrivce_maualinput_desc": "请粘贴授权流程最后的那个码,然后点击确认。",
"modal_googledrive_maualinput_notice": "正在尝试连接 Google 并更新授权信息......",
"modal_googledrive_maualinput_succ_notice": "很好!授权信息已更新!",
"modal_googledrive_maualinput_fail_notice": "更新授权信息失败。请稍后重试。",
"modal_googledriverevokeauth_step1": "第 1 步:访问以下网址,可以删除连接。",
"modal_googledriverevokeauth_step2": "第 2 步:点击以下按钮,从而清理本地的登录信息。",
"modal_googledriverevokeauth_clean": "清理本地登录信息",
"modal_googledriverevokeauth_clean_desc": "您需要点击此按钮。",
"modal_googledriverevokeauth_clean_button": "清理",
"modal_googledriverevokeauth_clean_notice": "已清理!",
"modal_googledriverevokeauth_clean_fail": "清理授权时候发生了错误。",
"modal_prorevokeauth": "点击这里和按照步骤取消授权。",
"modal_prorevokeauth_clean": "清理",
"modal_prorevokeauth_clean_desc": "清理本地授权记录",
@@ -20,6 +36,26 @@
"modal_proauth_maualinput_notice": "正在连接,请稍候......",
"modal_proauth_maualinput_conn_fail": "连接失败",
"settings_googledrive": "Google Drive (PRO) (beta)",
"settings_chooseservice_googledrive": "Google Drive (PRO) (beta)",
"settings_googledrive_disclaimer1": "声明:本插件不是 Google 的官方产品。只是用到了它的公开 API。",
"settings_googledrive_disclaimer2": "声明:您所输入的信息存储于本地。其它有害的或者出错的插件,是有可能读取到这些信息的。如果您发现任何不符合预期的 Google Drive 访问,请立刻在以下网站操作断开连接: https://myaccount.google.com/permissions 。",
"settings_googledrive_pro_desc": "<p><strong>!!这是 PRO(付费)功能! 您需要在线账号来使用此功能!!</strong><a href=\"#settings-pro\">向下滑</a>可以看到 PRO 账号的更多信息。)</p>",
"settings_googledrive_notshowuphint": "Google Drive 设置不可用",
"settings_googledrive_notshowuphint_desc": "Google Drive 设置不可用,因为您没有在 Remotely Save 账号里开启这个 PRO 功能。",
"settings_googledrive_notshowuphint_view_pro": "查看 PRO 相关设置",
"settings_googledrive_folder": "我们会在 Google Drive 创建此文件夹并同步内容进去: {{remoteBaseDir}} 。请不要手动在网站上创建。",
"settings_googledrive_revoke": "撤回鉴权",
"settings_googledrive_revoke_desc": "您现在已连接。如果想取消连接,请点击此按钮。",
"settings_googledrive_revoke_button": "撤回鉴权",
"settings_googledrive_auth": "鉴权",
"settings_googledrive_auth_desc": "鉴权.",
"settings_googledrive_auth_button": "鉴权",
"settings_googledrive_connect_succ": "很好!我们可连接上 Google Drive",
"settings_googledrive_connect_fail": "我们未能连接上 Google Drive。",
"settings_export_googledrive_button": "导出 Google Drive 部分",
"settings_pro": "账号(PRO 付费功能)",
"settings_pro_tutorial": "<p>使用 Remotely Save 的<stong>基本</strong>功能是<strong>免费的</strong>,而且<strong>不</strong>需要注册对应账号。</p><p>但是,您<strong>需要</strong>注册账号和对<strong>PRO</strong>功能<strong>付费</strong>使用,如智能处理冲突功能。</p><p>第一步:点击按钮从而注册和登录网站:<a href=\"https://remotelysave.com\">https://remotelysave.com</a>。注意:这和 Obsidian 官方账号无关,是不同的账号。</p><p>第二部:点击“连接”按钮,从而连接本设备和在线账号。",
"settings_pro_features": "功能",
+36
View File
@@ -7,6 +7,22 @@
"protocol_pro_connect_fail": "Remotely Save 官網返回錯誤。可能是網路連線不穩定。也可能是您拒絕了授權?",
"protocol_pro_connect_succ_revoke": "您已連線上賬號 {{email}}。如果要取消連線,請點選此按鈕。",
"modal_googledriveauth_tutorial": "<p>請訪問此網址,然後會進入授權流程。最後,您會看到一個碼,請複製貼上到這裡然後提交。</p>",
"modal_googledriveauth_copybutton": "點選以複製網址",
"modal_googledriveauth_copynotice": "網址已複製!",
"modal_googledrivce_maualinput": "網站上的碼",
"modal_googledrivce_maualinput_desc": "請貼上授權流程最後的那個碼,然後點選確認。",
"modal_googledrive_maualinput_notice": "正在嘗試連線 Google 並更新授權資訊......",
"modal_googledrive_maualinput_succ_notice": "很好!授權資訊已更新!",
"modal_googledrive_maualinput_fail_notice": "更新授權資訊失敗。請稍後重試。",
"modal_googledriverevokeauth_step1": "第 1 步:訪問以下網址,可以刪除連線。",
"modal_googledriverevokeauth_step2": "第 2 步:點選以下按鈕,從而清理本地的登入資訊。",
"modal_googledriverevokeauth_clean": "清理本地登入資訊",
"modal_googledriverevokeauth_clean_desc": "您需要點選此按鈕。",
"modal_googledriverevokeauth_clean_button": "清理",
"modal_googledriverevokeauth_clean_notice": "已清理!",
"modal_googledriverevokeauth_clean_fail": "清理授權時候發生了錯誤。",
"modal_prorevokeauth": "點選這裡和按照步驟取消授權。",
"modal_prorevokeauth_clean": "清理",
"modal_prorevokeauth_clean_desc": "清理本地授權記錄",
@@ -20,6 +36,26 @@
"modal_proauth_maualinput_notice": "正在連線,請稍候......",
"modal_proauth_maualinput_conn_fail": "連線失敗",
"settings_googledrive": "Google Drive (PRO) (beta)",
"settings_chooseservice_googledrive": "Google Drive (PRO) (beta)",
"settings_googledrive_disclaimer1": "宣告:本外掛不是 Google 的官方產品。只是用到了它的公開 API。",
"settings_googledrive_disclaimer2": "宣告:您所輸入的資訊儲存於本地。其它有害的或者出錯的外掛,是有可能讀取到這些資訊的。如果您發現任何不符合預期的 Google Drive 訪問,請立刻在以下網站操作斷開連線: https://myaccount.google.com/permissions 。",
"settings_googledrive_pro_desc": "<p><strong>!!這是 PRO(付費)功能! 您需要線上賬號來使用此功能!!</strong><a href=\"#settings-pro\">向下滑</a>可以看到 PRO 賬號的更多資訊。)</p>",
"settings_googledrive_notshowuphint": "Google Drive 設定不可用",
"settings_googledrive_notshowuphint_desc": "Google Drive 設定不可用,因為您沒有在 Remotely Save 賬號裡開啟這個 PRO 功能。",
"settings_googledrive_notshowuphint_view_pro": "檢視 PRO 相關設定",
"settings_googledrive_folder": "我們會在 Google Drive 建立此資料夾並同步內容進去: {{remoteBaseDir}} 。請不要手動在網站上建立。",
"settings_googledrive_revoke": "撤回鑑權",
"settings_googledrive_revoke_desc": "您現在已連線。如果想取消連線,請點選此按鈕。",
"settings_googledrive_revoke_button": "撤回鑑權",
"settings_googledrive_auth": "鑑權",
"settings_googledrive_auth_desc": "鑑權.",
"settings_googledrive_auth_button": "鑑權",
"settings_googledrive_connect_succ": "很好!我們可連線上 Google Drive",
"settings_googledrive_connect_fail": "我們未能連線上 Google Drive。",
"settings_export_googledrive_button": "匯出 Google Drive 部分",
"settings_pro": "賬號(PRO 付費功能)",
"settings_pro_tutorial": "<p>使用 Remotely Save 的<stong>基本</strong>功能是<strong>免費的</strong>,而且<strong>不</strong>需要註冊對應賬號。</p><p>但是,您<strong>需要</strong>註冊賬號和對<strong>PRO</strong>功能<strong>付費</strong>使用,如智慧處理衝突功能。</p><p>第一步:點選按鈕從而註冊和登入網站:<a href=\"https://remotelysave.com\">https://remotelysave.com</a>。注意:這和 Obsidian 官方賬號無關,是不同的賬號。</p><p>第二部:點選“連線”按鈕,從而連線本裝置和線上賬號。",
"settings_pro_features": "功能",
+377
View File
@@ -0,0 +1,377 @@
import cloneDeep from "lodash/cloneDeep";
import { type App, Modal, Notice, Setting } from "obsidian";
import { getClient } from "../../src/fsGetter";
import type { TransItemType } from "../../src/i18n";
import type RemotelySavePlugin from "../../src/main";
import { stringToFragment } from "../../src/misc";
import { ChangeRemoteBaseDirModal } from "../../src/settings";
import {
DEFAULT_GOOGLEDRIVE_CONFIG,
sendRefreshTokenReq,
} from "./fsGoogleDrive";
class GoogleDriveAuthModal extends Modal {
readonly plugin: RemotelySavePlugin;
readonly authDiv: HTMLDivElement;
readonly revokeAuthDiv: HTMLDivElement;
readonly revokeAuthSetting: Setting;
readonly t: (x: TransItemType, vars?: any) => string;
constructor(
app: App,
plugin: RemotelySavePlugin,
authDiv: HTMLDivElement,
revokeAuthDiv: HTMLDivElement,
revokeAuthSetting: Setting,
t: (x: TransItemType, vars?: any) => string
) {
super(app);
this.plugin = plugin;
this.authDiv = authDiv;
this.revokeAuthDiv = revokeAuthDiv;
this.revokeAuthSetting = revokeAuthSetting;
this.t = t;
}
async onOpen() {
const { contentEl } = this;
const t = this.t;
const authUrl = "https://remotelysave.com/auth/googledrive/start";
const div2 = contentEl.createDiv();
div2.createDiv({
text: stringToFragment(t("modal_googledriveauth_tutorial")),
});
div2.createEl(
"button",
{
text: t("modal_googledriveauth_copybutton"),
},
(el) => {
el.onclick = async () => {
await navigator.clipboard.writeText(authUrl);
new Notice(t("modal_googledriveauth_copynotice"));
};
}
);
contentEl.createEl("p").createEl("a", {
href: authUrl,
text: authUrl,
});
let refreshToken = "";
new Setting(contentEl)
.setName(t("modal_googledrivce_maualinput"))
.setDesc(t("modal_googledrivce_maualinput_desc"))
.addText((text) =>
text
.setPlaceholder("")
.setValue("")
.onChange((val) => {
refreshToken = val.trim();
})
)
.addButton(async (button) => {
button.setButtonText(t("submit"));
button.onClick(async () => {
new Notice(t("modal_googledrive_maualinput_notice"));
try {
if (this.plugin.settings.googledrive === undefined) {
this.plugin.settings.googledrive = cloneDeep(
DEFAULT_GOOGLEDRIVE_CONFIG
);
}
this.plugin.settings.googledrive.refreshToken = refreshToken;
this.plugin.settings.googledrive.accessToken = "access";
this.plugin.settings.googledrive.accessTokenExpiresAtTimeMs = 1;
this.plugin.settings.googledrive.accessTokenExpiresInMs = 1;
// TODO: abstraction leaking now, how to fix?
const k = await sendRefreshTokenReq(refreshToken);
const ts = Date.now();
this.plugin.settings.googledrive.accessToken = k.access_token;
this.plugin.settings.googledrive.accessTokenExpiresInMs =
k.expires_in * 1000;
this.plugin.settings.googledrive.accessTokenExpiresAtTimeMs =
ts + k.expires_in * 1000 - 60 * 2 * 1000;
await this.plugin.saveSettings();
// try to remove data in clipboard
await navigator.clipboard.writeText("");
new Notice(t("modal_googledrive_maualinput_succ_notice"));
} catch (e) {
console.error(e);
new Notice(t("modal_googledrive_maualinput_fail_notice"));
} finally {
this.authDiv.toggleClass(
"googledrive-auth-button-hide",
this.plugin.settings.googledrive.refreshToken !== ""
);
this.revokeAuthDiv.toggleClass(
"googledrive-revoke-auth-button-hide",
this.plugin.settings.googledrive.refreshToken === ""
);
this.close();
}
});
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
class GoogleDriveRevokeAuthModal extends Modal {
readonly plugin: RemotelySavePlugin;
readonly authDiv: HTMLDivElement;
readonly revokeAuthDiv: HTMLDivElement;
readonly t: (x: TransItemType, vars?: any) => string;
constructor(
app: App,
plugin: RemotelySavePlugin,
authDiv: HTMLDivElement,
revokeAuthDiv: HTMLDivElement,
t: (x: TransItemType, vars?: any) => string
) {
super(app);
this.plugin = plugin;
this.authDiv = authDiv;
this.revokeAuthDiv = revokeAuthDiv;
this.t = t;
}
async onOpen() {
const t = this.t;
const { contentEl } = this;
contentEl.createEl("p", {
text: t("modal_googledriverevokeauth_step1"),
});
const consentUrl = "https://myaccount.google.com/permissions";
contentEl.createEl("p").createEl("a", {
href: consentUrl,
text: consentUrl,
});
contentEl.createEl("p", {
text: t("modal_googledriverevokeauth_step2"),
});
new Setting(contentEl)
.setName(t("modal_googledriverevokeauth_clean"))
.setDesc(t("modal_googledriverevokeauth_clean_desc"))
.addButton(async (button) => {
button.setButtonText(t("modal_googledriverevokeauth_clean_button"));
button.onClick(async () => {
try {
this.plugin.settings.googledrive = cloneDeep(
DEFAULT_GOOGLEDRIVE_CONFIG
);
await this.plugin.saveSettings();
this.authDiv.toggleClass(
"googledrive-auth-button-hide",
this.plugin.settings.googledrive.refreshToken !== ""
);
this.revokeAuthDiv.toggleClass(
"googledrive-revoke-auth-button-hide",
this.plugin.settings.googledrive.refreshToken === ""
);
new Notice(t("modal_googledriverevokeauth_clean_notice"));
this.close();
} catch (err) {
console.error(err);
new Notice(t("modal_googledriverevokeauth_clean_fail"));
}
});
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
export const generateGoogleDriveSettingsPart = (
containerEl: HTMLElement,
t: (x: TransItemType, vars?: any) => string,
app: App,
plugin: RemotelySavePlugin,
saveUpdatedConfigFunc: () => Promise<any> | undefined
) => {
const googleDriveDiv = containerEl.createEl("div", {
cls: "googledrive-hide",
});
googleDriveDiv.toggleClass(
"googledrive-hide",
plugin.settings.serviceType !== "googledrive"
);
googleDriveDiv.createEl("h2", { text: t("settings_googledrive") });
const googleDriveLongDescDiv = googleDriveDiv.createEl("div", {
cls: "settings-long-desc",
});
for (const c of [
t("settings_googledrive_disclaimer1"),
t("settings_googledrive_disclaimer2"),
]) {
googleDriveLongDescDiv.createEl("p", {
text: c,
cls: "googledrive-disclaimer",
});
}
googleDriveLongDescDiv.createEl("p", {
text: t("settings_googledrive_folder", {
remoteBaseDir:
plugin.settings.googledrive.remoteBaseDir || app.vault.getName(),
}),
});
googleDriveLongDescDiv.createDiv({
text: stringToFragment(t("settings_googledrive_pro_desc")),
cls: "googledrive-disclaimer",
});
const googleDriveNotShowUpHintSetting = new Setting(googleDriveDiv)
.setName(t("settings_googledrive_notshowuphint"))
.setDesc(t("settings_googledrive_notshowuphint_desc"))
.addButton(async (button) => {
button.setButtonText(t("settings_googledrive_notshowuphint_view_pro"));
button.onClick(async () => {
window.location.href = "#settings-pro";
});
});
const googleDriveAllowedToUsedDiv = googleDriveDiv.createDiv();
// if pro enabled, show up; otherwise hide.
const allowGoogleDrive =
plugin.settings.pro?.enabledProFeatures.filter(
(x) => x.featureName === "feature-google_drive"
).length === 1;
console.debug(`allow to show up google drive settings? ${allowGoogleDrive}`);
if (allowGoogleDrive) {
googleDriveAllowedToUsedDiv.removeClass("googledrive-allow-to-use-hide");
googleDriveNotShowUpHintSetting.settingEl.addClass(
"googledrive-allow-to-use-hide"
);
} else {
googleDriveAllowedToUsedDiv.addClass("googledrive-allow-to-use-hide");
googleDriveNotShowUpHintSetting.settingEl.removeClass(
"googledrive-allow-to-use-hide"
);
}
const googleDriveSelectAuthDiv = googleDriveAllowedToUsedDiv.createDiv();
const googleDriveAuthDiv = googleDriveSelectAuthDiv.createDiv({
cls: "googledrive-auth-button-hide settings-auth-related",
});
const googleDriveRevokeAuthDiv = googleDriveSelectAuthDiv.createDiv({
cls: "googledrive-revoke-auth-button-hide settings-auth-related",
});
const googleDriveRevokeAuthSetting = new Setting(googleDriveRevokeAuthDiv)
.setName(t("settings_googledrive_revoke"))
.setDesc(t("settings_googledrive_revoke_desc"))
.addButton(async (button) => {
button.setButtonText(t("settings_googledrive_revoke_button"));
button.onClick(async () => {
new GoogleDriveRevokeAuthModal(
app,
plugin,
googleDriveAuthDiv,
googleDriveRevokeAuthDiv,
t
).open();
});
});
new Setting(googleDriveAuthDiv)
.setName(t("settings_googledrive_auth"))
.setDesc(t("settings_googledrive_auth_desc"))
.addButton(async (button) => {
button.setButtonText(t("settings_googledrive_auth_button"));
button.onClick(async () => {
const modal = new GoogleDriveAuthModal(
app,
plugin,
googleDriveAuthDiv,
googleDriveRevokeAuthDiv,
googleDriveRevokeAuthSetting,
t
);
plugin.oauth2Info.helperModal = modal;
plugin.oauth2Info.authDiv = googleDriveAuthDiv;
plugin.oauth2Info.revokeDiv = googleDriveRevokeAuthDiv;
plugin.oauth2Info.revokeAuthSetting = googleDriveRevokeAuthSetting;
modal.open();
});
});
googleDriveAuthDiv.toggleClass(
"googledrive-auth-button-hide",
plugin.settings.googledrive.refreshToken !== ""
);
googleDriveRevokeAuthDiv.toggleClass(
"googledrive-revoke-auth-button-hide",
plugin.settings.googledrive.refreshToken === ""
);
let newgoogleDriveRemoteBaseDir =
plugin.settings.googledrive.remoteBaseDir || "";
new Setting(googleDriveAllowedToUsedDiv)
.setName(t("settings_remotebasedir"))
.setDesc(t("settings_remotebasedir_desc"))
.addText((text) =>
text
.setPlaceholder(app.vault.getName())
.setValue(newgoogleDriveRemoteBaseDir)
.onChange((value) => {
newgoogleDriveRemoteBaseDir = value.trim();
})
)
.addButton((button) => {
button.setButtonText(t("confirm"));
button.onClick(() => {
new ChangeRemoteBaseDirModal(
app,
plugin,
newgoogleDriveRemoteBaseDir,
"googledrive"
).open();
});
});
new Setting(googleDriveAllowedToUsedDiv)
.setName(t("settings_checkonnectivity"))
.setDesc(t("settings_checkonnectivity_desc"))
.addButton(async (button) => {
button.setButtonText(t("settings_checkonnectivity_button"));
button.onClick(async () => {
new Notice(t("settings_checkonnectivity_checking"));
const client = getClient(plugin.settings, app.vault.getName(), () =>
plugin.saveSettings()
);
const errors = { msg: "" };
const res = await client.checkConnect((err: any) => {
errors.msg = `${err}`;
});
if (res) {
new Notice(t("settings_googledrive_connect_succ"));
} else {
new Notice(t("settings_googledrive_connect_fail"));
new Notice(errors.msg);
}
});
});
return {
googleDriveDiv: googleDriveDiv,
googleDriveAllowedToUsedDiv: googleDriveAllowedToUsedDiv,
googleDriveNotShowUpHintSetting: googleDriveNotShowUpHintSetting,
};
};
+28 -1
View File
@@ -150,6 +150,9 @@ export class ProAuthModal extends Modal {
);
this.plugin.oauth2Info.revokeDiv = undefined;
// try to remove data in clipboard
await navigator.clipboard.writeText("");
this.close();
} catch (err) {
console.error(err);
@@ -244,7 +247,9 @@ export const generateProSettingsPart = (
t: (x: TransItemType, vars?: any) => string,
app: App,
plugin: RemotelySavePlugin,
saveUpdatedConfigFunc: () => Promise<any> | undefined
saveUpdatedConfigFunc: () => Promise<any> | undefined,
googleDriveAllowedToUsedDiv: HTMLDivElement,
googleDriveNotShowUpHintSetting: Setting
) => {
proDiv
.createEl("h2", { text: t("settings_pro") })
@@ -290,6 +295,28 @@ export const generateProSettingsPart = (
})
)
);
const allowGoogleDrive =
plugin.settings.pro?.enabledProFeatures.filter(
(x) => x.featureName === "feature-google_drive"
).length === 1;
console.debug(
`allow to show up google drive settings? ${allowGoogleDrive}`
);
if (allowGoogleDrive) {
googleDriveAllowedToUsedDiv.removeClass(
"googledrive-allow-to-use-hide"
);
googleDriveNotShowUpHintSetting.settingEl.addClass(
"googledrive-allow-to-use-hide"
);
} else {
googleDriveAllowedToUsedDiv.addClass("googledrive-allow-to-use-hide");
googleDriveNotShowUpHintSetting.settingEl.removeClass(
"googledrive-allow-to-use-hide"
);
}
new Notice(t("settings_pro_features_refresh_succ"));
});
});