diff --git a/package.json b/package.json index a0247edc..85ad5c50 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "fs-extra": "^10.0.1", "highlight.js": "^11.5.1", "interactjs": "^1.10.11", + "lodash": "^4.17.21", "lowdb": "^3.0.0", "markdown-it": "^12.3.2", "markdown-it-link-attributes": "^4.0.0", diff --git a/src/main/preload.ts b/src/main/preload.ts index 03d2ba57..adfd1c5a 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -1,5 +1,5 @@ import { contextBridge, ipcRenderer } from 'electron' -import { isDbExist, migrate, move } from './services/db' +import { isDbExist, migrate, migrateFromSnippetsLab, move } from './services/db' import { store } from './store' import type { ElectronBridge } from '@shared/types/main' import { version } from '../../package.json' @@ -29,6 +29,7 @@ contextBridge.exposeInMainWorld('electron', { }, db: { migrate: path => migrate(path), + migrateFromSnippetsLab: path => migrateFromSnippetsLab(path), move: (from, to) => move(from, to), isExist: path => isDbExist(path) }, diff --git a/src/main/services/db/index.ts b/src/main/services/db/index.ts index 92b9d20a..8934d1c1 100644 --- a/src/main/services/db/index.ts +++ b/src/main/services/db/index.ts @@ -3,8 +3,12 @@ import fs from 'fs-extra' import readline from 'readline' import { nestedToFlat } from '../../utils' import { nanoid } from 'nanoid' -import type { Folder, Snippet, Tag } from '@shared/types/main/db' -import { oldLanguageMap } from '../../../renderer/components/editor/languages' +import type { DB, Folder, Snippet, Tag } from '@shared/types/main/db' +import { + oldLanguageMap, + languages +} from '../../../renderer/components/editor/languages' +import { snakeCase } from 'lodash' const DB_NAME = 'db.json' @@ -207,3 +211,113 @@ export const migrate = async (path: string) => { writeToFile(db) console.log('Migrate is done') } + +export const migrateFromSnippetsLab = (path: string) => { + interface SLFragment { + Content: string + 'Date Created': string + 'Date Modified': string + Note: string + Title: string + Language: string + } + interface SLSnippet { + 'Date Created': string + 'Date Modified': string + Folder: string + Title: string + Fragments: SLFragment[] + Tags: string[] + } + + interface SnippetsLabDbJSON { + Snippets: SLSnippet[] + } + + const INBOX = 'Uncategorized' + + const file = fs.readFileSync(path, 'utf-8') + const json = JSON.parse(file) as SnippetsLabDbJSON + + const folders = new Set() + const tags = new Set() + + const db: DB = { + folders: [...DEFAULT_SYSTEM_FOLDERS], + snippets: [], + tags: [] + } + + json.Snippets.forEach(i => { + if (i.Folder) folders.add(i.Folder) + + if (i.Tags.length) { + i.Tags.forEach(t => tags.add(t)) + } + }) + + folders.forEach(i => { + if (i === INBOX) return + db.folders.push({ + id: nanoid(8), + name: i, + defaultLanguage: 'plain_text', + parentId: null, + isOpen: false, + isSystem: false, + createdAt: new Date().valueOf(), + updatedAt: new Date().valueOf() + }) + }) + + tags.forEach(i => { + db.tags.push({ + id: nanoid(8), + name: i, + createdAt: new Date().valueOf(), + updatedAt: new Date().valueOf() + }) + }) + + json.Snippets.forEach(i => { + const folderId = db.folders.find(f => f.name === i.Folder)?.id || '' + const tagsIds: string[] = [] + + if (i.Tags.length) { + i.Tags.forEach(t => { + const id = db.tags.find(_t => _t.name === t)?.id + if (id) tagsIds.push(id) + }) + } + + const snippet: Snippet = { + id: nanoid(8), + name: i.Title, + content: [], + folderId, + tagsIds, + isDeleted: false, + isFavorites: false, + createdAt: new Date(i['Date Created']).valueOf(), + updatedAt: new Date(i['Date Modified']).valueOf() + } + + if (i.Fragments.length) { + i.Fragments.forEach(f => { + const _language = snakeCase(f.Language.toLowerCase()) + const language = languages.find(i => i.value === _language)?.value + + snippet.content.push({ + label: f.Title, + value: f.Content, + language: language || 'plain_text' + }) + }) + } + + db.snippets.push(snippet) + }) + + writeToFile(db) + console.log('Migrate is done') +} diff --git a/src/main/services/ipc/dialog.ts b/src/main/services/ipc/dialog.ts index 196ee4dc..f2ccc188 100644 --- a/src/main/services/ipc/dialog.ts +++ b/src/main/services/ipc/dialog.ts @@ -1,11 +1,14 @@ -import type { MessageBoxRequest } from '@shared/types/main' +import type { DialogRequest, MessageBoxRequest } from '@shared/types/main' import { dialog, ipcMain } from 'electron' export const subscribeToDialog = () => { - ipcMain.handle('main:open-dialog', () => { + ipcMain.handle('main:open-dialog', (event, payload) => { return new Promise(resolve => { + const { properties, filters } = payload + const dir = dialog.showOpenDialogSync({ - properties: ['openDirectory', 'createDirectory'] + properties: properties || ['openDirectory', 'createDirectory'], + filters: filters || [{ name: '*', extensions: ['json'] }] }) if (dir) { diff --git a/src/renderer/components/preferences/Storage.vue b/src/renderer/components/preferences/StoragePreferences.vue similarity index 60% rename from src/renderer/components/preferences/Storage.vue rename to src/renderer/components/preferences/StoragePreferences.vue index 536a7312..075fb447 100644 --- a/src/renderer/components/preferences/Storage.vue +++ b/src/renderer/components/preferences/StoragePreferences.vue @@ -19,10 +19,21 @@ - Open folder + From massCode v1.0 + + + From SnippetsLab @@ -36,7 +47,7 @@ import { ipc, store, db, track } from '@/electron' import { useFolderStore } from '@/store/folders' import { useSnippetStore } from '@/store/snippets' -import type { MessageBoxRequest } from '@shared/types/main' +import type { MessageBoxRequest, DialogRequest } from '@shared/types/main' import { ref } from 'vue' const snippetStore = useSnippetStore() @@ -95,12 +106,20 @@ const onClickMigrate = async () => { try { const path = await ipc.invoke('main:open-dialog', {}) + + if (!path) return + await db.migrate(path) + ipc.invoke('main:restart-api', {}) + + resetStore() + await snippetStore.getSnippets() + ipc.invoke('main:notification', { body: 'DB successfully migrated.' }) - snippetStore.getSnippets() + track('app/migrate') } catch (err) { const e = err as Error @@ -111,22 +130,64 @@ const onClickMigrate = async () => { } } +const onClickMigrateFromSnippetsLab = async () => { + const state = await ipc.invoke( + 'main:open-message-box', + { + message: 'Are you sure you want to migrate from SnippetsLab', + detail: 'During migrate, the current library will be overwritten.', + buttons: ['Confirm', 'Cancel'] + } + ) + + if (!state) return + + try { + const path = await ipc.invoke('main:open-dialog', { + properties: ['openFile'] + }) + + if (!path) return + + db.migrateFromSnippetsLab(path) + + ipc.invoke('main:restart-api', {}) + + resetStore() + await snippetStore.getSnippets() + + ipc.invoke('main:notification', { + body: 'DB successfully migrated.' + }) + + track('app/migrate', 'from-snippets-lab') + } catch (err) { + const e = err as Error + ipc.invoke('main:notification', { + body: e.message + }) + console.error(err) + } +} + const setStorageAndRestartApi = (path: string, reset?: boolean) => { storagePath.value = path store.preferences.set('storagePath', path) - if (reset) { - store.app.delete('selectedFolderAlias') - store.app.delete('selectedFolderId') - store.app.delete('selectedFolderIds') - store.app.delete('selectedSnippetId') - - snippetStore.$reset() - folderStore.$reset() - } + if (reset) resetStore() ipc.invoke('main:restart-api', {}) } + +const resetStore = () => { + store.app.delete('selectedFolderAlias') + store.app.delete('selectedFolderId') + store.app.delete('selectedFolderIds') + store.app.delete('selectedSnippetId') + + snippetStore.$reset() + folderStore.$reset() +} diff --git a/src/renderer/views/Preferences.vue b/src/renderer/views/Preferences.vue index 551044b1..fedaea2e 100644 --- a/src/renderer/views/Preferences.vue +++ b/src/renderer/views/Preferences.vue @@ -12,7 +12,7 @@ name="Storage" value="storage" > - + Promise + migrateFromSnippetsLab: (path: string) => void move: (from: string, to: string) => Promise isExist: (path: string) => boolean }