From 433922c72acfed5caacc57f5014cba077dc9be48 Mon Sep 17 00:00:00 2001 From: Dale de Silva Date: Sun, 8 Jan 2023 18:31:54 +1100 Subject: [PATCH] Refactored into multiple files --- esbuild.config.mjs | 4 +- main.ts | 791 ------------------ src/logic/import-logic.ts | 300 +++++++ src/logic/string-processes.ts | 35 + src/logic/types.ts | 5 + src/main.ts | 91 ++ src/modals/import-modal/import-modal.ts | 192 +++++ .../import-progress-modal.ts | 107 +++ {static => src/static}/.hotreload | 0 {static => src/static}/manifest.json | 2 +- styles.scss => src/styles.scss | 0 src/tabs/settings-tab/settings-tab.ts | 59 ++ tsconfig.json | 2 +- 13 files changed, 793 insertions(+), 795 deletions(-) delete mode 100644 main.ts create mode 100644 src/logic/import-logic.ts create mode 100644 src/logic/string-processes.ts create mode 100644 src/logic/types.ts create mode 100644 src/main.ts create mode 100644 src/modals/import-modal/import-modal.ts create mode 100644 src/modals/import-progress-modal/import-progress-modal.ts rename {static => src/static}/.hotreload (100%) rename {static => src/static}/manifest.json (93%) rename styles.scss => src/styles.scss (100%) create mode 100644 src/tabs/settings-tab/settings-tab.ts diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 44da230..7afec53 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -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'], }, }), diff --git a/main.ts b/main.ts deleted file mode 100644 index 158c951..0000000 --- a/main.ts +++ /dev/null @@ -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; - attachments?: Array; - 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 = []; - 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 = []; - - if (e.dataTransfer.items) { - // DataTransferItems is supporter in this browser - const items = [...e.dataTransfer.items]; - - for(let i=0; i ) { - 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 { - 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) { - - 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 { - - const root = plugin.app.vault.getRoot(); - let importFolder: TFolder | undefined; - - // Find the folder if it exists - for(let i=0; i { - - const importFolder = await getImportFolder() - let assetFolder: TFolder | undefined; - - // Find the folder if it exists - for(let i=0; i { - - 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 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 { -// 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 { - 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; -} \ No newline at end of file diff --git a/src/logic/import-logic.ts b/src/logic/import-logic.ts new file mode 100644 index 0000000..a98c83e --- /dev/null +++ b/src/logic/import-logic.ts @@ -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; + attachments?: Array; + title: string; + userEditedTimestampUsec: number; +} + + + + + + + + + + + + +async function createNewEmptyMdFile(vault: Vault, path: string, options: DataWriteOptions, version: number = 1) : Promise { + 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 { + + const root = vault.getRoot(); + let importFolder: TFolder | undefined; + + // Find the folder if it exists + for(let i=0; i { + + const importFolder = await getImportFolder(vault) + let assetFolder: TFolder | undefined; + + // Find the folder if it exists + for(let i=0; i) { + + 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 { + + // 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 str, options); // In docs, but not in class + + + return resolve(fileRef); + + + } + + }) + + + +} + + +async function importBinaryFile(vault: Vault, folder: TFolder, file: File) : Promise { + 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); + + +} \ No newline at end of file diff --git a/src/logic/string-processes.ts b/src/logic/string-processes.ts new file mode 100644 index 0000000..11089fd --- /dev/null +++ b/src/logic/string-processes.ts @@ -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; +} \ No newline at end of file diff --git a/src/logic/types.ts b/src/logic/types.ts new file mode 100644 index 0000000..cf1ee95 --- /dev/null +++ b/src/logic/types.ts @@ -0,0 +1,5 @@ +export interface MyPluginSettings { + hideOpenVaultButton: boolean; + vaultNames: [string]; + vaultLinks: [string]; +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..c4d2ecc --- /dev/null +++ b/src/main.ts @@ -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); + } +} + + diff --git a/src/modals/import-modal/import-modal.ts b/src/modals/import-modal/import-modal.ts new file mode 100644 index 0000000..bb4aeb5 --- /dev/null +++ b/src/modals/import-modal/import-modal.ts @@ -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 = []; + 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 = []; + + if (e.dataTransfer.items) { + // DataTransferItems is supporter in this browser + const items = [...e.dataTransfer.items]; + + for(let i=0; i ) { + 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 { + 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(); + } +} + + + + diff --git a/static/.hotreload b/src/static/.hotreload similarity index 100% rename from static/.hotreload rename to src/static/.hotreload diff --git a/static/manifest.json b/src/static/manifest.json similarity index 93% rename from static/manifest.json rename to src/static/manifest.json index a99a99c..d69edd1 100644 --- a/static/manifest.json +++ b/src/static/manifest.json @@ -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", diff --git a/styles.scss b/src/styles.scss similarity index 100% rename from styles.scss rename to src/styles.scss diff --git a/src/tabs/settings-tab/settings-tab.ts b/src/tabs/settings-tab/settings-tab.ts new file mode 100644 index 0000000..2961e06 --- /dev/null +++ b/src/tabs/settings-tab/settings-tab.ts @@ -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 => {}); + + + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 2d6fbdf..2399fcb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "isolatedModules": true, "strictNullChecks": true, "lib": [ - "DOM", + "DOM", "dom.iterable", "ES5", "ES6", "ES7"