mirror of
https://github.com/daledesilva/obsidian_google-keep-import.git
synced 2026-07-22 07:50:23 +00:00
Refactored into multiple files
This commit is contained in:
parent
78c40f1739
commit
433922c72a
13 changed files with 793 additions and 795 deletions
|
|
@ -17,7 +17,7 @@ esbuild.build({
|
|||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ['main.ts', 'styles.scss'],
|
||||
entryPoints: ['./src/main.ts', './src/styles.scss'],
|
||||
bundle: true,
|
||||
external: [
|
||||
'obsidian',
|
||||
|
|
@ -48,7 +48,7 @@ esbuild.build({
|
|||
copy({
|
||||
resolveFrom: 'cwd', // Returns name of current working directory
|
||||
assets: {
|
||||
from: ['./static/**/*'],
|
||||
from: ['./src/static/**/*'],
|
||||
to: ['./dist'],
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
791
main.ts
791
main.ts
|
|
@ -1,791 +0,0 @@
|
|||
import { fileSyntax } from 'esbuild-sass-plugin/lib/utils';
|
||||
import { App, DataWriteOptions, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, TFolder, Vault } from 'obsidian';
|
||||
|
||||
|
||||
|
||||
const IMPORT_FOLDER = 'Keep Imports';
|
||||
const ASSET_FOLDER = 'Attachments'
|
||||
|
||||
var plugin: MyPlugin;
|
||||
var successCount = 0;
|
||||
var failCount = 0;
|
||||
|
||||
interface KeepListItem {
|
||||
text: string;
|
||||
isChecked: boolean;
|
||||
}
|
||||
interface KeepAttachment {
|
||||
filePath: string;
|
||||
mimetype: string;
|
||||
}
|
||||
interface KeepJson {
|
||||
color: string;
|
||||
createdTimestampUsec: number;
|
||||
isArchived: boolean;
|
||||
isPinned: boolean;
|
||||
isTrashed: boolean;
|
||||
textContent?: string;
|
||||
listContent?: Array<KeepListItem>;
|
||||
attachments?: Array<KeepAttachment>;
|
||||
title: string;
|
||||
userEditedTimestampUsec: number;
|
||||
}
|
||||
|
||||
|
||||
interface MyPluginSettings {
|
||||
hideOpenVaultButton: boolean;
|
||||
vaultNames: [string];
|
||||
vaultLinks: [string];
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: MyPluginSettings = {
|
||||
hideOpenVaultButton: false,
|
||||
vaultNames: ['name'],
|
||||
vaultLinks: ['utl'],
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default class MyPlugin extends Plugin {
|
||||
settings: MyPluginSettings;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
plugin = this; // TODO: Not sure if this is a good idea or not
|
||||
|
||||
// This creates an icon in the left ribbon.
|
||||
const ribbonIconEl = this.addRibbonIcon('dice', 'Open Philsophy Vault', (evt: MouseEvent) => {
|
||||
// Called when the user clicks the icon.
|
||||
new Notice('Opening Philosophy Vault');
|
||||
window.open('obsidian://vault/Philosophy/');
|
||||
});
|
||||
// Perform additional things with the ribbon
|
||||
ribbonIconEl.addClass('my-plugin-ribbon-class');
|
||||
|
||||
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
|
||||
const statusBarItemEl = this.addStatusBarItem();
|
||||
statusBarItemEl.setText('Status Bar Text');
|
||||
|
||||
|
||||
|
||||
this.addCommand({
|
||||
id: 'ublik-om_import-google-keep-jsons',
|
||||
name: 'Import backup from Google Keep',
|
||||
callback: () => {
|
||||
new StartImportModal(this.app).open();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// This adds an editor command that can perform some operation on the current editor instance
|
||||
this.addCommand({
|
||||
id: 'sample-editor-command',
|
||||
name: 'Sample editor command',
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
console.log(editor.getSelection());
|
||||
editor.replaceSelection('Sample Editor Command');
|
||||
}
|
||||
});
|
||||
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
||||
|
||||
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
|
||||
// Using this function will automatically remove the event listener when this plugin is disabled.
|
||||
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
|
||||
console.log('click', evt);
|
||||
});
|
||||
|
||||
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
|
||||
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
class StartImportModal extends Modal {
|
||||
result: string;
|
||||
duplicateNotes: number = 0;
|
||||
noteSpan: HTMLSpanElement;
|
||||
assetSpan: HTMLSpanElement;
|
||||
fileBacklog: Array<File> = [];
|
||||
uploadInput: HTMLInputElement;
|
||||
modalActions: Setting;
|
||||
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const {titleEl, contentEl} = this;
|
||||
|
||||
titleEl.setText('Import Google Keep backup');
|
||||
|
||||
contentEl.createEl('p', {text: 'Here you can upload a set of jsons output from a Google Keep backup.'});
|
||||
contentEl.createEl('p', {text: 'Upload each json one at a time or all together. You should also upload any attachments in the backup as well such as png\'s jpgs, etc.'});
|
||||
contentEl.createEl('p', {text: 'If you import attachments or jsons separately and close this dialog, they will will automatically link together once their counterparts are imported later provided you haven\'t changed the names of attachments or modified the markdown embeds in the notes.'});
|
||||
|
||||
|
||||
const dropFrame = contentEl.createEl('div', {cls: 'uo_drop-frame'});
|
||||
|
||||
const dropFrameText = dropFrame.createEl('p', { text: 'Drag your files here or ' });
|
||||
// const linkText = dropFrameText.createEl('a', {text: 'browse local files'})
|
||||
dropFrameText.createEl('label', {
|
||||
text: 'browse local files',
|
||||
attr: {
|
||||
'class': 'uo_file-label',
|
||||
'for': 'uo_file',
|
||||
}
|
||||
})
|
||||
this.uploadInput = dropFrameText.createEl('input', {
|
||||
type: 'file',
|
||||
attr: {
|
||||
'multiple': true,
|
||||
'id': 'uo_file',
|
||||
'accept': '.json, .jpg, .png, .3gp',
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const summaryP = contentEl.createEl('p', {cls: 'uo_before-import-summary'});
|
||||
summaryP.createEl('span', {text: `notes: `});
|
||||
this.noteSpan = summaryP.createEl('span', {cls: 'uo_import-number', text: `0`});
|
||||
summaryP.createEl('span', {text: ` | attachments: `});
|
||||
this.assetSpan = summaryP.createEl('span', {cls: 'uo_import-number', text: `0`});
|
||||
|
||||
|
||||
this.modalActions = new Setting(contentEl).addButton(btn => {
|
||||
btn.setClass('uo_button');
|
||||
btn.setCta();
|
||||
btn.setButtonText('Start Import');
|
||||
btn.setDisabled(true);
|
||||
btn.onClick( (e) => {
|
||||
this.close();
|
||||
importFiles( this.fileBacklog );
|
||||
})
|
||||
})
|
||||
|
||||
this.uploadInput.addEventListener('change', () => {
|
||||
// Add imports to accumulative array
|
||||
this.addToFilesBacklog( Object.values(this.uploadInput.files as FileList) );
|
||||
});
|
||||
|
||||
dropFrame.addEventListener('dragenter', (e) => {
|
||||
dropFrame.addClass('uo_drag-over-active');
|
||||
});
|
||||
|
||||
dropFrame.addEventListener('dragover', (e) => {
|
||||
// Prevent default to allow drop
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
dropFrame.addEventListener('dragleave', (e) => {
|
||||
dropFrame.removeClass('uo_drag-over-active');
|
||||
});
|
||||
|
||||
dropFrame.addEventListener('drop', (e) => {
|
||||
// Prevent default to stop browser opening file
|
||||
e.preventDefault();
|
||||
|
||||
dropFrame.removeClass('uo_drag-over-active');
|
||||
|
||||
const files: Array<File> = [];
|
||||
|
||||
if (e.dataTransfer.items) {
|
||||
// DataTransferItems is supporter in this browser
|
||||
const items = [...e.dataTransfer.items];
|
||||
|
||||
for(let i=0; i<items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
files.push(file);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Use DataTransfer interface to access the file(s)
|
||||
files.push(...e.dataTransfer.files);
|
||||
}
|
||||
this.addToFilesBacklog(files);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
addToFilesBacklog( files: Array<File> ) {
|
||||
let newFiles = 0;
|
||||
let duplicateFiles = 0;
|
||||
|
||||
// Add non-duplicates to backlog
|
||||
files.forEach( (file) => {
|
||||
if( this.backlogContains(file) ) {
|
||||
duplicateFiles++;
|
||||
} else {
|
||||
this.fileBacklog.push(file);
|
||||
newFiles++;
|
||||
}
|
||||
})
|
||||
|
||||
if(duplicateFiles>0) new Notice(`${duplicateFiles} ${singleOrPlural(duplicateFiles, 'file')} ignored because ${singleOrPlural(duplicateFiles, 'it\'s', 'they\'re')} already in the import list.`, 9000);
|
||||
new Notice(`${newFiles} new ${singleOrPlural(newFiles, 'file')} queued for import.`, 10000);
|
||||
|
||||
// Erase references in upload component to prepare for new set
|
||||
this.uploadInput.files = null;
|
||||
|
||||
// Update summary numbers
|
||||
const breakdown = this.getFilesBacklogBreakdown();
|
||||
this.noteSpan.setText(`${breakdown.notes}`);
|
||||
this.assetSpan.setText(`${breakdown.assets}`);
|
||||
|
||||
// Activate start button
|
||||
const importBtn = this.modalActions.components[0];
|
||||
importBtn.setDisabled(false);
|
||||
}
|
||||
|
||||
backlogContains(file: File) {
|
||||
for(let i=0; i<this.fileBacklog.length; i++) {
|
||||
// NOTE: Path isn't necessarily guaranteed in all environments.
|
||||
// Could do a name and content comparison for jsons, and a so too for binary if possible.
|
||||
// Maybe even just a content comparison. For not this is fine.
|
||||
if((file as any).path) {
|
||||
if((file as any).path == (this.fileBacklog[i] as any).path) return true;
|
||||
}
|
||||
}
|
||||
// If no duplicates found
|
||||
return false;
|
||||
}
|
||||
|
||||
getFilesBacklogBreakdown(): { notes : number, assets: number } {
|
||||
let notes = 0;
|
||||
let assets = 0;
|
||||
|
||||
for(let i=0; i<this.fileBacklog.length; i++) {
|
||||
const file = this.fileBacklog[i] as File;
|
||||
|
||||
switch(file.type) {
|
||||
case 'application/json': notes++; break;
|
||||
default: assets++; break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
notes,
|
||||
assets
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onClose() {
|
||||
const {titleEl, contentEl} = this;
|
||||
titleEl.empty();
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ImportProgressModal extends Modal {
|
||||
result: string;
|
||||
bar: HTMLDivElement;
|
||||
remainingSpan: HTMLSpanElement;
|
||||
failedSpan: HTMLSpanElement;
|
||||
importedSpan: HTMLSpanElement;
|
||||
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const {titleEl, contentEl} = this;
|
||||
|
||||
titleEl.setText('Import in progress');
|
||||
|
||||
const progressBarEl = contentEl.createEl('div', {cls: 'uo_progress-bar'});
|
||||
this.bar = progressBarEl.createEl('div', {cls: 'uo_bar'});
|
||||
|
||||
|
||||
const summaryEl = contentEl.createDiv('uo_during-import-summary');
|
||||
let bubbleEl;
|
||||
let pBubbleEl
|
||||
|
||||
bubbleEl = summaryEl.createDiv('uo_import-remaining');
|
||||
pBubbleEl = bubbleEl.createEl('p');
|
||||
this.remainingSpan = pBubbleEl.createEl('span', {cls: 'uo_import-number', text: `-`});
|
||||
pBubbleEl.createEl('br');
|
||||
pBubbleEl.createEl('span', {cls: 'uo_import-label', text: `remaining`});
|
||||
|
||||
bubbleEl = summaryEl.createDiv('uo_import-failed');
|
||||
pBubbleEl = bubbleEl.createEl('p');
|
||||
this.failedSpan = pBubbleEl.createEl('span', {cls: 'uo_import-number', text: `${failCount}`});
|
||||
pBubbleEl.createEl('br');
|
||||
pBubbleEl.createEl('span', {cls: 'uo_import-label', text: `failed/skipped`});
|
||||
|
||||
bubbleEl = summaryEl.createDiv('uo_import-imported');
|
||||
pBubbleEl = bubbleEl.createEl('p');
|
||||
this.importedSpan = pBubbleEl.createEl('span', {cls: 'uo_import-number', text: `${successCount}`});
|
||||
pBubbleEl.createEl('br');
|
||||
pBubbleEl.createEl('span', {cls: 'uo_import-label', text: `imported`});
|
||||
|
||||
}
|
||||
|
||||
public updateProgress(options: {successCount: number, failCount: number, totalImports: number}) {
|
||||
const {
|
||||
successCount,
|
||||
failCount,
|
||||
totalImports
|
||||
} = options;
|
||||
|
||||
// Update bar visual
|
||||
const perc = (successCount + failCount)/totalImports * 100;
|
||||
this.bar.setAttr('style', `width: ${perc}%`);
|
||||
|
||||
// Update text
|
||||
this.remainingSpan.setText(`${totalImports-successCount-failCount}`);
|
||||
this.failedSpan.setText(`${failCount}`);
|
||||
this.importedSpan.setText(`${successCount}`);
|
||||
|
||||
// Update modal if finished
|
||||
if(perc == 100) {
|
||||
|
||||
const modalActions = new Setting(this.contentEl).addButton(btn => {
|
||||
btn.setClass('uo_button');
|
||||
// btn.setCta();
|
||||
btn.setButtonText('Close');
|
||||
btn.onClick( (e) => {
|
||||
this.close();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const {titleEl, contentEl} = this;
|
||||
titleEl.empty();
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async function importFiles(files: Array<Object>) {
|
||||
|
||||
const importProgressModal = new ImportProgressModal(plugin.app)
|
||||
importProgressModal.open();
|
||||
|
||||
const importFolder = await getImportFolder();
|
||||
const assetFolder = await getAssetFolder();
|
||||
|
||||
successCount = 0;
|
||||
failCount = 0;
|
||||
|
||||
updateProgressBar({
|
||||
totalImports: files.length,
|
||||
modal: importProgressModal,
|
||||
})
|
||||
|
||||
for(let i=0; i<files.length; i++) {
|
||||
const file = files[i] as File;
|
||||
|
||||
switch(file.type) {
|
||||
case 'application/json': await importJson(file, importFolder); break;
|
||||
case 'image/png': await importBinaryFile(file, assetFolder); break;
|
||||
case 'video/3gpp': await importBinaryFile(file, assetFolder); break;
|
||||
case 'image/jpeg': await importBinaryFile(file, assetFolder); break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function updateProgressBar(options: {totalImports: number, modal: ImportProgressModal}) {
|
||||
const {totalImports, modal} = options;
|
||||
const barEl = modal.bar;
|
||||
|
||||
modal.updateProgress({
|
||||
successCount,
|
||||
failCount,
|
||||
totalImports
|
||||
})
|
||||
|
||||
if(successCount + failCount == totalImports) {
|
||||
return
|
||||
}
|
||||
|
||||
requestAnimationFrame( function() {
|
||||
updateProgressBar(options);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function getImportFolder(): Promise<TFolder> {
|
||||
|
||||
const root = plugin.app.vault.getRoot();
|
||||
let importFolder: TFolder | undefined;
|
||||
|
||||
// Find the folder if it exists
|
||||
for(let i=0; i<root.children.length; i++) {
|
||||
if(root.children[i].path == IMPORT_FOLDER) {
|
||||
importFolder = root.children[i] as TFolder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the folder if it doesn't exist
|
||||
if(importFolder === undefined) {
|
||||
await plugin.app.vault.createFolder(IMPORT_FOLDER);
|
||||
importFolder = await getImportFolder()
|
||||
}
|
||||
|
||||
return importFolder;
|
||||
}
|
||||
|
||||
|
||||
async function getAssetFolder(): Promise<TFolder> {
|
||||
|
||||
const importFolder = await getImportFolder()
|
||||
let assetFolder: TFolder | undefined;
|
||||
|
||||
// Find the folder if it exists
|
||||
for(let i=0; i<importFolder.children.length; i++) {
|
||||
if(importFolder.children[i].name == ASSET_FOLDER) {
|
||||
assetFolder = importFolder.children[i] as TFolder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the folder if it doesn't exist
|
||||
if(assetFolder === undefined) {
|
||||
await plugin.app.vault.createFolder(`${importFolder.path}/${ASSET_FOLDER}`);
|
||||
assetFolder = await getAssetFolder()
|
||||
}
|
||||
|
||||
return assetFolder;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function importJson(file: File, folder: TFolder) {
|
||||
|
||||
// setting up the reader
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(file as Blob,'UTF-8');
|
||||
|
||||
return new Promise( (resolve, reject) => {
|
||||
|
||||
reader.onerror = reject;
|
||||
reader.onload = async (readerEvent) => {
|
||||
|
||||
// Bail if the file hasn't been interpreted properly
|
||||
if(!readerEvent || !readerEvent.target) {
|
||||
return Promise.reject(new Error(`Something went wrong reading json: ${file.name}`));
|
||||
}
|
||||
|
||||
const content: KeepJson = JSON.parse(readerEvent.target.result as string);
|
||||
let path: string = `${folder.path}/${filenameSanitize(content.title || file.name)}`; // TODO: Strip file extension from filename
|
||||
let fileRef: TFile;
|
||||
|
||||
|
||||
// Create new file
|
||||
try {
|
||||
// path = await getUnusedFilename(path);
|
||||
// fileRef = await plugin.app.vault.create(`${path}.md`, '');
|
||||
fileRef = await createNewEmptyMdFile(path, {});
|
||||
} catch (error) {
|
||||
failCount++;
|
||||
return Promise.reject(new Error(`Error creating new file ${path} (from ${file.name}. Error: ${error})`));
|
||||
}
|
||||
|
||||
// Add in tags to represent Keep properties
|
||||
try {
|
||||
await plugin.app.vault.append(fileRef, `#Keep/Colour/${content.color} `);
|
||||
content.isPinned ? await plugin.app.vault.append(fileRef, `#Keep/Pinned `) : null;
|
||||
content.attachments ? await plugin.app.vault.append(fileRef, `#Keep/Attachments `) : null;
|
||||
content.isArchived ? await plugin.app.vault.append(fileRef, `#Keep/Archived `) : null;
|
||||
content.isTrashed ? await plugin.app.vault.append(fileRef, `#Keep/Trashed `) : null;
|
||||
} catch (error) {
|
||||
failCount++;
|
||||
return Promise.reject(Error(`Error adding tags to new file ${path} (from ${file.name})`));
|
||||
}
|
||||
|
||||
// Add in text content
|
||||
try {
|
||||
if(content.textContent) {
|
||||
await plugin.app.vault.append(fileRef, `\n\n`);
|
||||
await plugin.app.vault.append(fileRef, `${content.textContent}\n`);
|
||||
}
|
||||
} catch (error) {
|
||||
failCount++;
|
||||
return Promise.reject(new Error(`Error adding paragraph content to new file ${path} (from ${file.name})`));
|
||||
}
|
||||
|
||||
// Add in text content if check box
|
||||
try {
|
||||
if(content.listContent) {
|
||||
await plugin.app.vault.append(fileRef, `\n\n`);
|
||||
for(let i=0; i<content.listContent.length; i++) {
|
||||
const listItem = content.listContent[i];
|
||||
|
||||
// Skip to next line if this one is blank
|
||||
if(!listItem.text) continue;
|
||||
|
||||
let listItemContent = `- [${listItem.isChecked ? 'X' : ' '}] ${listItem.text}\n`;
|
||||
await plugin.app.vault.append(fileRef, listItemContent);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
failCount++;
|
||||
return Promise.reject(new Error(`Error adding list content to new file ${path} (from ${file.name})`));
|
||||
}
|
||||
|
||||
// Embed attachments
|
||||
// NOTE: The files for these may not have been created yet, but since it's just markdown text, they can be created after.
|
||||
if(content.attachments) {
|
||||
for(let i=0; i<content.attachments.length; i++) {
|
||||
const attachment = content.attachments[i];
|
||||
try {
|
||||
await plugin.app.vault.append(fileRef, `\n\n![[${attachment.filePath}]]`);
|
||||
} catch (error) {
|
||||
failCount++;
|
||||
return Promise.reject(new Error(`Error embedding attachment ${attachment.filePath} to new file ${path} (from ${file.name})`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update created and modified date to match Keep data
|
||||
const options: DataWriteOptions = {
|
||||
ctime: content.createdTimestampUsec/1000,
|
||||
mtime: content.userEditedTimestampUsec/1000
|
||||
}
|
||||
await plugin.app.vault.append(fileRef, '', options);
|
||||
// await plugin.app.vault.process(fileRef, (str) => str, options); // In docs, but not in class
|
||||
|
||||
|
||||
successCount++;
|
||||
resolve(fileRef);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
/*
|
||||
JSON
|
||||
//////////
|
||||
{
|
||||
"color": "DEFAULT",
|
||||
"isTrashed": false,
|
||||
"isPinned": false,
|
||||
"isArchived": true,
|
||||
"title": "",
|
||||
"userEditedTimestampUsec": 1462811176816000,
|
||||
"createdTimestampUsec": 1462763160359000
|
||||
|
||||
"textContent": "",
|
||||
OR
|
||||
"listContent": [
|
||||
{
|
||||
"text": "",
|
||||
"isChecked": false
|
||||
},
|
||||
{
|
||||
"text": "",
|
||||
"isChecked": false
|
||||
},
|
||||
{
|
||||
"text": "175.32.124.133\n",
|
||||
"isChecked": false
|
||||
}
|
||||
],
|
||||
|
||||
OPTIONAL:
|
||||
"attachments": [
|
||||
{
|
||||
"filePath": "16271b560b6.ba20e87f973dd142.png",
|
||||
"mimetype": "image/png"
|
||||
}
|
||||
],
|
||||
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
async function importBinaryFile(file: File, folder: TFolder) {
|
||||
|
||||
const path: string = `${folder.path}/${file.name}`;
|
||||
|
||||
try {
|
||||
const fileRef: TFile = await plugin.app.vault.createBinary(path, await file.arrayBuffer());
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
failCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
Potential formats:
|
||||
- jpg
|
||||
- jpeg
|
||||
- png
|
||||
- 3gp (Audio/video recording)
|
||||
*/
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function filenameSanitize(str: string) {
|
||||
|
||||
// Remove /
|
||||
let newArr = str.split('/');
|
||||
let newStr = newArr.join();
|
||||
|
||||
// Remove \
|
||||
newArr = newStr.split('\\');
|
||||
newStr = newArr.join();
|
||||
|
||||
// Remove :
|
||||
newArr = newStr.split(':');
|
||||
newStr = newArr.join();
|
||||
|
||||
return newStr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class SampleSettingTab extends PluginSettingTab {
|
||||
plugin: MyPlugin;
|
||||
|
||||
constructor(app: App, plugin: MyPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const {containerEl} = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl('h2', {text: 'Settings for my awesome plugin.'});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Hide "Open Other Vault" button')
|
||||
.setDesc('This will hide the button at the bottom left that returns you to the Vault Selector screen. This is mostly useful for mobile users that find this button disruptive to flow.')
|
||||
.addToggle(value => value
|
||||
.setValue(this.plugin.settings.hideOpenVaultButton)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hideOpenVaultButton = !value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
|
||||
// new Setting(contentEl)
|
||||
// .setName("Name")
|
||||
// .addText((text) =>
|
||||
// text.onChange((value) => {
|
||||
// this.result = value
|
||||
// }));
|
||||
|
||||
// new Setting(contentEl)
|
||||
// .addColorPicker(color => {});
|
||||
|
||||
// new Setting(contentEl)
|
||||
// .addExtraButton(btn => {});
|
||||
|
||||
// new Setting(contentEl)
|
||||
// .addMomentFormat(test => {})
|
||||
|
||||
// new Setting(contentEl)
|
||||
// .addSearch(test => {});
|
||||
|
||||
// new Setting(contentEl)
|
||||
// .addSlider(test => {});
|
||||
|
||||
// new Setting(contentEl)
|
||||
// .addTextArea(test => {});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const singleOrPlural = (count: number, singleVersion: string, pluralVersion?: string) => {
|
||||
if(count == 1 || count == -1) {
|
||||
return singleVersion;
|
||||
} else {
|
||||
if(pluralVersion) {
|
||||
// custom plural version passed in
|
||||
return pluralVersion;
|
||||
} else {
|
||||
// just add an s
|
||||
return `${singleVersion}s`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// async function getUnusedFilename(path: string) : Promise<string> {
|
||||
// let newPath: string = path;
|
||||
// let fileRef = plugin.app.vault.getAbstractFileByPath(`${newPath}.md`); // TODO: This is case sensitive. So I've replaced it with the function below.
|
||||
|
||||
// if(fileRef == null || fileRef == undefined) {
|
||||
// // File doesn't exist yet, so just return the same path
|
||||
// console.log('newPath', newPath);
|
||||
// console.log('doesn\'t exist', fileRef);
|
||||
|
||||
// } else {
|
||||
// let version = 2;
|
||||
// while(fileRef && fileRef instanceof TFile) {
|
||||
// newPath = `${path} v${version}`;
|
||||
// fileRef = plugin.app.vault.getAbstractFileByPath(`${newPath}.md`);
|
||||
// version++;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return newPath;
|
||||
// }
|
||||
|
||||
|
||||
|
||||
async function createNewEmptyMdFile(path: string, options: DataWriteOptions, version: number = 1) : Promise<TFile> {
|
||||
let fileRef: TFile;
|
||||
|
||||
try {
|
||||
if(version == 1) {
|
||||
fileRef = await plugin.app.vault.create(`${path}.md`, '', options);
|
||||
} else {
|
||||
fileRef = await plugin.app.vault.create(`${path} (${version}).md`, '', options);
|
||||
}
|
||||
|
||||
} catch(error) {
|
||||
; fileRef = await createNewEmptyMdFile(path, options, version+1);
|
||||
|
||||
}
|
||||
|
||||
|
||||
return fileRef;
|
||||
}
|
||||
300
src/logic/import-logic.ts
Normal file
300
src/logic/import-logic.ts
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
import { DataWriteOptions, TFile, TFolder, Vault } from "obsidian";
|
||||
import MyPlugin, { ASSET_FOLDER, IMPORT_FOLDER } from "src/main";
|
||||
import { ImportProgressModal, updateProgress } from "src/modals/import-progress-modal/import-progress-modal";
|
||||
import { filenameSanitize } from "./string-processes";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Types definitions
|
||||
////////////////////
|
||||
|
||||
|
||||
interface KeepListItem {
|
||||
text: string;
|
||||
isChecked: boolean;
|
||||
}
|
||||
|
||||
interface KeepAttachment {
|
||||
filePath: string;
|
||||
mimetype: string;
|
||||
}
|
||||
|
||||
interface KeepJson {
|
||||
color: string;
|
||||
createdTimestampUsec: number;
|
||||
isArchived: boolean;
|
||||
isPinned: boolean;
|
||||
isTrashed: boolean;
|
||||
textContent?: string;
|
||||
listContent?: Array<KeepListItem>;
|
||||
attachments?: Array<KeepAttachment>;
|
||||
title: string;
|
||||
userEditedTimestampUsec: number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async function createNewEmptyMdFile(vault: Vault, path: string, options: DataWriteOptions, version: number = 1) : Promise<TFile> {
|
||||
let fileRef: TFile;
|
||||
|
||||
try {
|
||||
if(version == 1) {
|
||||
fileRef = await vault.create(`${path}.md`, '', options);
|
||||
} else {
|
||||
fileRef = await vault.create(`${path} (${version}).md`, '', options);
|
||||
}
|
||||
|
||||
} catch(error) {
|
||||
; fileRef = await createNewEmptyMdFile(vault, path, options, version+1);
|
||||
|
||||
}
|
||||
|
||||
|
||||
return fileRef;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async function getImportFolder(vault: Vault): Promise<TFolder> {
|
||||
|
||||
const root = vault.getRoot();
|
||||
let importFolder: TFolder | undefined;
|
||||
|
||||
// Find the folder if it exists
|
||||
for(let i=0; i<root.children.length; i++) {
|
||||
if(root.children[i].path == IMPORT_FOLDER) {
|
||||
importFolder = root.children[i] as TFolder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the folder if it doesn't exist
|
||||
if(importFolder === undefined) {
|
||||
await vault.createFolder(IMPORT_FOLDER);
|
||||
importFolder = await getImportFolder(vault)
|
||||
}
|
||||
|
||||
return importFolder;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async function getAssetFolder(vault: Vault): Promise<TFolder> {
|
||||
|
||||
const importFolder = await getImportFolder(vault)
|
||||
let assetFolder: TFolder | undefined;
|
||||
|
||||
// Find the folder if it exists
|
||||
for(let i=0; i<importFolder.children.length; i++) {
|
||||
if(importFolder.children[i].name == ASSET_FOLDER) {
|
||||
assetFolder = importFolder.children[i] as TFolder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the folder if it doesn't exist
|
||||
if(assetFolder === undefined) {
|
||||
await vault.createFolder(`${importFolder.path}/${ASSET_FOLDER}`);
|
||||
assetFolder = await getAssetFolder(vault)
|
||||
}
|
||||
|
||||
return assetFolder;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export async function importFiles(plugin: MyPlugin, files: Array<Object>) {
|
||||
|
||||
const importProgressModal = new ImportProgressModal(plugin)
|
||||
importProgressModal.open();
|
||||
|
||||
const importFolder = await getImportFolder(plugin.app.vault);
|
||||
const assetFolder = await getAssetFolder(plugin.app.vault); // TODO: Only do this if there is an attachement to be imported
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
updateProgress({
|
||||
successCount,
|
||||
failCount,
|
||||
totalImports: files.length,
|
||||
modal: importProgressModal,
|
||||
})
|
||||
|
||||
for(let i=0; i<files.length; i++) {
|
||||
const file = files[i] as File;
|
||||
let fileRef: TFile | Error;
|
||||
|
||||
if(file.type === 'image/png') {
|
||||
fileRef = await importBinaryFile(plugin.app.vault, assetFolder, file);
|
||||
|
||||
} else if(file.type === 'video/3gpp') {
|
||||
fileRef = await importBinaryFile(plugin.app.vault, assetFolder, file);
|
||||
|
||||
} else if(file.type === 'image/jpeg') {
|
||||
fileRef = await importBinaryFile(plugin.app.vault, assetFolder, file);
|
||||
|
||||
} else { // file.type === 'application/json'
|
||||
fileRef = await importJson(plugin.app.vault, importFolder, file);
|
||||
|
||||
}
|
||||
|
||||
if(fileRef instanceof Error) {
|
||||
failCount++;
|
||||
} else {
|
||||
successCount++;
|
||||
}
|
||||
|
||||
updateProgress({
|
||||
successCount,
|
||||
failCount,
|
||||
totalImports: files.length,
|
||||
modal: importProgressModal,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async function importJson(vault: Vault, folder: TFolder, file: File) : Promise<TFile> {
|
||||
|
||||
// setting up the reader
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(file as Blob,'UTF-8');
|
||||
|
||||
// TODO: This composition is confusing - Attempt to simplify
|
||||
return new Promise( (resolve, reject) => {
|
||||
|
||||
reader.onerror = reject;
|
||||
reader.onload = async (readerEvent) => {
|
||||
|
||||
// Bail if the file hasn't been interpreted properly
|
||||
if(!readerEvent || !readerEvent.target) {
|
||||
return reject(new Error(`Something went wrong reading json: ${file.name}`));
|
||||
}
|
||||
|
||||
const content: KeepJson = JSON.parse(readerEvent.target.result as string);
|
||||
let path: string = `${folder.path}/${filenameSanitize(content.title || file.name)}`; // TODO: Strip file extension from filename
|
||||
let fileRef: TFile;
|
||||
|
||||
|
||||
// Create new file
|
||||
try {
|
||||
// path = await getUnusedFilename(path);
|
||||
// fileRef = await plugin.app.vault.create(`${path}.md`, '');
|
||||
fileRef = await createNewEmptyMdFile(vault, path, {});
|
||||
} catch (error) {
|
||||
return reject(new Error(`Error creating new file ${path} (from ${file.name}. Error: ${error})`));
|
||||
}
|
||||
|
||||
// Add in tags to represent Keep properties
|
||||
try {
|
||||
await vault.append(fileRef, `#Keep/Colour/${content.color} `);
|
||||
content.isPinned ? await vault.append(fileRef, `#Keep/Pinned `) : null;
|
||||
content.attachments ? await vault.append(fileRef, `#Keep/Attachments `) : null;
|
||||
content.isArchived ? await vault.append(fileRef, `#Keep/Archived `) : null;
|
||||
content.isTrashed ? await vault.append(fileRef, `#Keep/Trashed `) : null;
|
||||
} catch (error) {
|
||||
return reject(Error(`Error adding tags to new file ${path} (from ${file.name})`));
|
||||
}
|
||||
|
||||
// Add in text content
|
||||
try {
|
||||
if(content.textContent) {
|
||||
await vault.append(fileRef, `\n\n`);
|
||||
await vault.append(fileRef, `${content.textContent}\n`);
|
||||
}
|
||||
} catch (error) {
|
||||
return reject(new Error(`Error adding paragraph content to new file ${path} (from ${file.name})`));
|
||||
}
|
||||
|
||||
// Add in text content if check box
|
||||
try {
|
||||
if(content.listContent) {
|
||||
await vault.append(fileRef, `\n\n`);
|
||||
for(let i=0; i<content.listContent.length; i++) {
|
||||
const listItem = content.listContent[i];
|
||||
|
||||
// Skip to next line if this one is blank
|
||||
if(!listItem.text) continue;
|
||||
|
||||
let listItemContent = `- [${listItem.isChecked ? 'X' : ' '}] ${listItem.text}\n`;
|
||||
await vault.append(fileRef, listItemContent);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return reject(new Error(`Error adding list content to new file ${path} (from ${file.name})`));
|
||||
}
|
||||
|
||||
// Embed attachments
|
||||
// NOTE: The files for these may not have been created yet, but since it's just markdown text, they can be created after.
|
||||
if(content.attachments) {
|
||||
for(let i=0; i<content.attachments.length; i++) {
|
||||
const attachment = content.attachments[i];
|
||||
try {
|
||||
await vault.append(fileRef, `\n\n![[${attachment.filePath}]]`);
|
||||
} catch (error) {
|
||||
return reject(new Error(`Error embedding attachment ${attachment.filePath} to new file ${path} (from ${file.name})`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update created and modified date to match Keep data
|
||||
const options: DataWriteOptions = {
|
||||
ctime: content.createdTimestampUsec/1000,
|
||||
mtime: content.userEditedTimestampUsec/1000
|
||||
}
|
||||
await vault.append(fileRef, '', options);
|
||||
// await plugin.app.vault.process(fileRef, (str) => str, options); // In docs, but not in class
|
||||
|
||||
|
||||
return resolve(fileRef);
|
||||
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function importBinaryFile(vault: Vault, folder: TFolder, file: File) : Promise<TFile> {
|
||||
let fileRef: TFile;
|
||||
const path: string = `${folder.path}/${file.name}`;
|
||||
|
||||
try {
|
||||
fileRef = await vault.createBinary(path, await file.arrayBuffer());
|
||||
} catch (error) {
|
||||
return Promise.reject(new Error(`Error creating attachment (Binary file) ${path} (from ${file.name})`));
|
||||
}
|
||||
|
||||
return Promise.resolve(fileRef);
|
||||
|
||||
|
||||
}
|
||||
35
src/logic/string-processes.ts
Normal file
35
src/logic/string-processes.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
|
||||
|
||||
|
||||
export const singleOrPlural = (count: number, singleVersion: string, pluralVersion?: string) => {
|
||||
if(count == 1 || count == -1) {
|
||||
return singleVersion;
|
||||
} else {
|
||||
if(pluralVersion) {
|
||||
// custom plural version passed in
|
||||
return pluralVersion;
|
||||
} else {
|
||||
// just add an s
|
||||
return `${singleVersion}s`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function filenameSanitize(str: string) {
|
||||
|
||||
// Remove /
|
||||
let newArr = str.split('/');
|
||||
let newStr = newArr.join();
|
||||
|
||||
// Remove \
|
||||
newArr = newStr.split('\\');
|
||||
newStr = newArr.join();
|
||||
|
||||
// Remove :
|
||||
newArr = newStr.split(':');
|
||||
newStr = newArr.join();
|
||||
|
||||
return newStr;
|
||||
}
|
||||
5
src/logic/types.ts
Normal file
5
src/logic/types.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export interface MyPluginSettings {
|
||||
hideOpenVaultButton: boolean;
|
||||
vaultNames: [string];
|
||||
vaultLinks: [string];
|
||||
}
|
||||
91
src/main.ts
Normal file
91
src/main.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { fileSyntax } from 'esbuild-sass-plugin/lib/utils';
|
||||
import { App, DataWriteOptions, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, TFolder, Vault } from 'obsidian';
|
||||
import { MyPluginSettings } from './logic/types';
|
||||
import { StartImportModal } from './modals/import-modal/import-modal';
|
||||
import { SampleSettingTab } from './tabs/settings-tab/settings-tab';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// TODO: Put these inside the plugin settings
|
||||
export const IMPORT_FOLDER = 'Keep Imports';
|
||||
export const ASSET_FOLDER = 'Attachments'
|
||||
|
||||
const DEFAULT_SETTINGS: MyPluginSettings = {
|
||||
hideOpenVaultButton: false,
|
||||
vaultNames: ['name'],
|
||||
vaultLinks: ['utl'],
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default class MyPlugin extends Plugin {
|
||||
settings: MyPluginSettings;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// This creates an icon in the left ribbon.
|
||||
const ribbonIconEl = this.addRibbonIcon('dice', 'Open Philsophy Vault', (evt: MouseEvent) => {
|
||||
// Called when the user clicks the icon.
|
||||
new Notice('Opening Philosophy Vault');
|
||||
window.open('obsidian://vault/Philosophy/');
|
||||
});
|
||||
// Perform additional things with the ribbon
|
||||
ribbonIconEl.addClass('my-plugin-ribbon-class');
|
||||
|
||||
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
|
||||
const statusBarItemEl = this.addStatusBarItem();
|
||||
statusBarItemEl.setText('Status Bar Text');
|
||||
|
||||
|
||||
|
||||
this.addCommand({
|
||||
id: 'ublik-om_import-google-keep-jsons',
|
||||
name: 'Import backup from Google Keep',
|
||||
callback: () => {
|
||||
new StartImportModal(this).open();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// This adds an editor command that can perform some operation on the current editor instance
|
||||
this.addCommand({
|
||||
id: 'sample-editor-command',
|
||||
name: 'Sample editor command',
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
console.log(editor.getSelection());
|
||||
editor.replaceSelection('Sample Editor Command');
|
||||
}
|
||||
});
|
||||
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
||||
|
||||
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
|
||||
// Using this function will automatically remove the event listener when this plugin is disabled.
|
||||
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
|
||||
console.log('click', evt);
|
||||
});
|
||||
|
||||
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
|
||||
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
192
src/modals/import-modal/import-modal.ts
Normal file
192
src/modals/import-modal/import-modal.ts
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import { App, Modal, Notice, Setting } from "obsidian";
|
||||
import { importFiles } from "src/logic/import-logic";
|
||||
import { singleOrPlural } from "src/logic/string-processes";
|
||||
import MyPlugin from "src/main";
|
||||
|
||||
|
||||
|
||||
|
||||
export class StartImportModal extends Modal {
|
||||
plugin: MyPlugin;
|
||||
result: string;
|
||||
duplicateNotes: number = 0;
|
||||
noteSpan: HTMLSpanElement;
|
||||
assetSpan: HTMLSpanElement;
|
||||
fileBacklog: Array<File> = [];
|
||||
uploadInput: HTMLInputElement;
|
||||
modalActions: Setting;
|
||||
|
||||
constructor(plugin: MyPlugin) {
|
||||
super(plugin.app);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const {titleEl, contentEl} = this;
|
||||
|
||||
titleEl.setText('Import Google Keep backup');
|
||||
|
||||
contentEl.createEl('p', {text: 'Here you can upload a set of jsons output from a Google Keep backup.'});
|
||||
contentEl.createEl('p', {text: 'Upload each json one at a time or all together. You should also upload any attachments in the backup as well such as png\'s jpgs, etc.'});
|
||||
contentEl.createEl('p', {text: 'If you import attachments or jsons separately and close this dialog, they will will automatically link together once their counterparts are imported later provided you haven\'t changed the names of attachments or modified the markdown embeds in the notes.'});
|
||||
|
||||
|
||||
const dropFrame = contentEl.createEl('div', {cls: 'uo_drop-frame'});
|
||||
|
||||
const dropFrameText = dropFrame.createEl('p', { text: 'Drag your files here or ' });
|
||||
// const linkText = dropFrameText.createEl('a', {text: 'browse local files'})
|
||||
dropFrameText.createEl('label', {
|
||||
text: 'browse local files',
|
||||
attr: {
|
||||
'class': 'uo_file-label',
|
||||
'for': 'uo_file',
|
||||
}
|
||||
})
|
||||
this.uploadInput = dropFrameText.createEl('input', {
|
||||
type: 'file',
|
||||
attr: {
|
||||
'multiple': true,
|
||||
'id': 'uo_file',
|
||||
'accept': '.json, .jpg, .png, .3gp',
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const summaryP = contentEl.createEl('p', {cls: 'uo_before-import-summary'});
|
||||
summaryP.createEl('span', {text: `notes: `});
|
||||
this.noteSpan = summaryP.createEl('span', {cls: 'uo_import-number', text: `0`});
|
||||
summaryP.createEl('span', {text: ` | attachments: `});
|
||||
this.assetSpan = summaryP.createEl('span', {cls: 'uo_import-number', text: `0`});
|
||||
|
||||
|
||||
this.modalActions = new Setting(contentEl).addButton(btn => {
|
||||
btn.setClass('uo_button');
|
||||
btn.setCta();
|
||||
btn.setButtonText('Start Import');
|
||||
btn.setDisabled(true);
|
||||
btn.onClick( (e) => {
|
||||
this.close();
|
||||
console.log("click to start import");
|
||||
importFiles(this.plugin, this.fileBacklog );
|
||||
})
|
||||
})
|
||||
|
||||
this.uploadInput.addEventListener('change', () => {
|
||||
// Add imports to accumulative array
|
||||
this.addToFilesBacklog( Object.values(this.uploadInput.files as FileList) );
|
||||
});
|
||||
|
||||
dropFrame.addEventListener('dragenter', (e) => {
|
||||
dropFrame.addClass('uo_drag-over-active');
|
||||
});
|
||||
|
||||
dropFrame.addEventListener('dragover', (e) => {
|
||||
// Prevent default to allow drop
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
dropFrame.addEventListener('dragleave', (e) => {
|
||||
dropFrame.removeClass('uo_drag-over-active');
|
||||
});
|
||||
|
||||
dropFrame.addEventListener('drop', (e) => {
|
||||
// Prevent default to stop browser opening file
|
||||
e.preventDefault();
|
||||
|
||||
if(e.dataTransfer === null) return;
|
||||
|
||||
dropFrame.removeClass('uo_drag-over-active');
|
||||
|
||||
const files: Array<File> = [];
|
||||
|
||||
if (e.dataTransfer.items) {
|
||||
// DataTransferItems is supporter in this browser
|
||||
const items = [...e.dataTransfer.items];
|
||||
|
||||
for(let i=0; i<items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.kind === 'file') {
|
||||
const file: File | null = item.getAsFile();
|
||||
if(file) files.push(file);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Use DataTransfer interface to access the file(s)
|
||||
files.push(...e.dataTransfer.files);
|
||||
}
|
||||
this.addToFilesBacklog(files);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
addToFilesBacklog( files: Array<File> ) {
|
||||
let newFiles = 0;
|
||||
let duplicateFiles = 0;
|
||||
|
||||
// Add non-duplicates to backlog
|
||||
files.forEach( (file) => {
|
||||
if( this.backlogContains(file) ) {
|
||||
duplicateFiles++;
|
||||
} else {
|
||||
this.fileBacklog.push(file);
|
||||
newFiles++;
|
||||
}
|
||||
})
|
||||
|
||||
if(duplicateFiles>0) new Notice(`${duplicateFiles} ${singleOrPlural(duplicateFiles, 'file')} ignored because ${singleOrPlural(duplicateFiles, 'it\'s', 'they\'re')} already in the import list.`, 9000);
|
||||
new Notice(`${newFiles} new ${singleOrPlural(newFiles, 'file')} queued for import.`, 10000);
|
||||
|
||||
// Erase references in upload component to prepare for new set
|
||||
this.uploadInput.files = null;
|
||||
|
||||
// Update summary numbers
|
||||
const breakdown = this.getFilesBacklogBreakdown();
|
||||
this.noteSpan.setText(`${breakdown.notes}`);
|
||||
this.assetSpan.setText(`${breakdown.assets}`);
|
||||
|
||||
// Activate start button
|
||||
const importBtn = this.modalActions.components[0];
|
||||
importBtn.setDisabled(false);
|
||||
}
|
||||
|
||||
backlogContains(file: File) {
|
||||
for(let i=0; i<this.fileBacklog.length; i++) {
|
||||
// NOTE: Path isn't necessarily guaranteed in all environments.
|
||||
// Could do a name and content comparison for jsons, and a so too for binary if possible.
|
||||
// Maybe even just a content comparison. For not this is fine.
|
||||
if((file as any).path) {
|
||||
if((file as any).path == (this.fileBacklog[i] as any).path) return true;
|
||||
}
|
||||
}
|
||||
// If no duplicates found
|
||||
return false;
|
||||
}
|
||||
|
||||
getFilesBacklogBreakdown(): { notes : number, assets: number } {
|
||||
let notes = 0;
|
||||
let assets = 0;
|
||||
|
||||
for(let i=0; i<this.fileBacklog.length; i++) {
|
||||
const file = this.fileBacklog[i] as File;
|
||||
|
||||
switch(file.type) {
|
||||
case 'application/json': notes++; break;
|
||||
default: assets++; break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
notes,
|
||||
assets
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onClose() {
|
||||
const {titleEl, contentEl} = this;
|
||||
titleEl.empty();
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
107
src/modals/import-progress-modal/import-progress-modal.ts
Normal file
107
src/modals/import-progress-modal/import-progress-modal.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { App, Modal, Setting, TFile, TFolder } from "obsidian";
|
||||
import MyPlugin from "src/main";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export class ImportProgressModal extends Modal {
|
||||
result: string;
|
||||
bar: HTMLDivElement;
|
||||
remainingSpan: HTMLSpanElement;
|
||||
failedSpan: HTMLSpanElement;
|
||||
importedSpan: HTMLSpanElement;
|
||||
|
||||
constructor(plugin: MyPlugin) {
|
||||
super(plugin.app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const {titleEl, contentEl} = this;
|
||||
|
||||
titleEl.setText('Import in progress');
|
||||
|
||||
const progressBarEl = contentEl.createEl('div', {cls: 'uo_progress-bar'});
|
||||
this.bar = progressBarEl.createEl('div', {cls: 'uo_bar'});
|
||||
|
||||
|
||||
const summaryEl = contentEl.createDiv('uo_during-import-summary');
|
||||
let bubbleEl;
|
||||
let pBubbleEl
|
||||
|
||||
bubbleEl = summaryEl.createDiv('uo_import-remaining');
|
||||
pBubbleEl = bubbleEl.createEl('p');
|
||||
this.remainingSpan = pBubbleEl.createEl('span', {cls: 'uo_import-number', text: `-`});
|
||||
pBubbleEl.createEl('br');
|
||||
pBubbleEl.createEl('span', {cls: 'uo_import-label', text: `remaining`});
|
||||
|
||||
bubbleEl = summaryEl.createDiv('uo_import-failed');
|
||||
pBubbleEl = bubbleEl.createEl('p');
|
||||
this.failedSpan = pBubbleEl.createEl('span', {cls: 'uo_import-number', text: `-`});
|
||||
pBubbleEl.createEl('br');
|
||||
pBubbleEl.createEl('span', {cls: 'uo_import-label', text: `failed/skipped`});
|
||||
|
||||
bubbleEl = summaryEl.createDiv('uo_import-imported');
|
||||
pBubbleEl = bubbleEl.createEl('p');
|
||||
this.importedSpan = pBubbleEl.createEl('span', {cls: 'uo_import-number', text: `-`});
|
||||
pBubbleEl.createEl('br');
|
||||
pBubbleEl.createEl('span', {cls: 'uo_import-label', text: `imported`});
|
||||
|
||||
}
|
||||
|
||||
public updateProgressVisuals(options: {successCount: number, failCount: number, totalImports: number}) {
|
||||
const {
|
||||
successCount,
|
||||
failCount,
|
||||
totalImports
|
||||
} = options;
|
||||
|
||||
// Update bar visual
|
||||
const perc = (successCount + failCount)/totalImports * 100;
|
||||
this.bar.setAttr('style', `width: ${perc}%`);
|
||||
|
||||
// Update text
|
||||
this.remainingSpan.setText(`${totalImports-successCount-failCount}`);
|
||||
this.failedSpan.setText(`${failCount}`);
|
||||
this.importedSpan.setText(`${successCount}`);
|
||||
|
||||
}
|
||||
|
||||
public applyCompletedState() {
|
||||
const modalActions = new Setting(this.contentEl).addButton(btn => {
|
||||
btn.setCta();
|
||||
btn.setClass('uo_button');
|
||||
btn.setButtonText('Close');
|
||||
btn.onClick( (e) => {
|
||||
this.close();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const {titleEl, contentEl} = this;
|
||||
titleEl.empty();
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function updateProgress(options: {successCount: number, failCount: number, totalImports: number, modal: ImportProgressModal}) {
|
||||
const {successCount, failCount, totalImports, modal} = options;
|
||||
|
||||
modal.updateProgressVisuals({
|
||||
successCount,
|
||||
failCount,
|
||||
totalImports
|
||||
})
|
||||
|
||||
if(successCount + failCount == totalImports) {
|
||||
modal.applyCompletedState();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "ublik-om_keep-import",
|
||||
"name": "Keep Import",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.5",
|
||||
"minAppVersion": "1.00.0",
|
||||
"description": "Allows import of Google Keep backup json files",
|
||||
"author": "Dale de Silva",
|
||||
59
src/tabs/settings-tab/settings-tab.ts
Normal file
59
src/tabs/settings-tab/settings-tab.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
import MyPlugin from "src/main";
|
||||
|
||||
|
||||
|
||||
export class SampleSettingTab extends PluginSettingTab {
|
||||
plugin: MyPlugin;
|
||||
|
||||
constructor(app: App, plugin: MyPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const {containerEl} = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl('h2', {text: 'Settings for my awesome plugin.'});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Hide "Open Other Vault" button')
|
||||
.setDesc('This will hide the button at the bottom left that returns you to the Vault Selector screen. This is mostly useful for mobile users that find this button disruptive to flow.')
|
||||
.addToggle(value => value
|
||||
.setValue(this.plugin.settings.hideOpenVaultButton)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hideOpenVaultButton = !value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
|
||||
// new Setting(contentEl)
|
||||
// .setName("Name")
|
||||
// .addText((text) =>
|
||||
// text.onChange((value) => {
|
||||
// this.result = value
|
||||
// }));
|
||||
|
||||
// new Setting(contentEl)
|
||||
// .addColorPicker(color => {});
|
||||
|
||||
// new Setting(contentEl)
|
||||
// .addExtraButton(btn => {});
|
||||
|
||||
// new Setting(contentEl)
|
||||
// .addMomentFormat(test => {})
|
||||
|
||||
// new Setting(contentEl)
|
||||
// .addSearch(test => {});
|
||||
|
||||
// new Setting(contentEl)
|
||||
// .addSlider(test => {});
|
||||
|
||||
// new Setting(contentEl)
|
||||
// .addTextArea(test => {});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM", "dom.iterable",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
|
|
|
|||
Loading…
Reference in a new issue