diff options
author | Daniel Weipert <code@drogueronin.de> | 2021-07-09 15:12:50 +0200 |
---|---|---|
committer | Daniel Weipert <code@drogueronin.de> | 2021-07-09 15:12:50 +0200 |
commit | 30c8c2f2b05bab8b962b51c0faeb282980324a5c (patch) | |
tree | 0088c91d7e44c76a966601d325bc53d1fdbc657e | |
parent | 6563602fb39db8ae9a976784b62538cbcf3de108 (diff) |
Splits UI into steps and enhances log messages
-rw-r--r-- | app/imap.js | 49 | ||||
-rw-r--r-- | app/index.html | 4 | ||||
-rw-r--r-- | app/src/components/Folders.vue | 7 | ||||
-rw-r--r-- | app/src/components/Panel.vue | 55 | ||||
-rw-r--r-- | app/src/components/Select.vue | 81 | ||||
-rw-r--r-- | app/src/pages/Home.vue | 129 | ||||
-rw-r--r-- | app/src/pages/Steps/1-From.vue | 32 | ||||
-rw-r--r-- | app/src/pages/Steps/2-To.vue | 51 | ||||
-rw-r--r-- | app/src/pages/Steps/3-Migrate.vue | 111 | ||||
-rw-r--r-- | app/src/router.js | 18 | ||||
-rw-r--r-- | package-lock.json | 5 | ||||
-rw-r--r-- | package.json | 1 |
12 files changed, 394 insertions, 149 deletions
diff --git a/app/imap.js b/app/imap.js index 4041089..d8caa0e 100644 --- a/app/imap.js +++ b/app/imap.js @@ -1,9 +1,10 @@ const { ipcMain } = require('electron'); const { ImapFlow } = require('imapflow'); -ipcMain.on('imap:listTree:from', listTreeFrom); -ipcMain.on('imap:listTree:to', listTreeTo); -ipcMain.on('imap:migrate', migrate); +ipcMain.on('imap:connect', apiConnect); +ipcMain.on('imap:listTree:from', apiListTreeFrom); +ipcMain.on('imap:listTree:to', apiListTreeTo); +ipcMain.on('imap:migrate', apiMigrate); /** * Connect to server @@ -51,7 +52,22 @@ ImapFlow.prototype.fetchArray = async function (range, query, options = {}) { * @param {IpcMainEvent} event * @param {object} options */ -async function listTreeFrom (event, options) { +async function apiConnect (event, options) { + try { + const client = await connect(options); + + event.reply('imap:connect:reply', true); + await client.logout(); + } catch (e) { + event.reply('imap:connect:error', e); + } +} + +/** + * @param {IpcMainEvent} event + * @param {object} options + */ +async function apiListTreeFrom (event, options) { try { const client = await connect(options); @@ -66,7 +82,7 @@ async function listTreeFrom (event, options) { * @param {IpcMainEvent} event * @param {object} options */ -async function listTreeTo (event, options) { +async function apiListTreeTo (event, options) { try { const client = await connect(options); @@ -82,7 +98,7 @@ async function listTreeTo (event, options) { * @param {object} from * @param {object} to */ -async function migrate (event, { from, to }) { +async function apiMigrate (event, { from, to }) { const fromClient = await connect(from); const toClient = await connect(to); @@ -108,17 +124,24 @@ async function migrate (event, { from, to }) { event.reply('imap:migrate:progress', '- Comparing messages'); const msgs = fromMsgs.filter((fromMsg) => !toMsgs.some((toMsg) => Buffer.compare(toMsg.source, fromMsg.source) === 0)); - event.reply('imap:migrate:progress', `- Message status: Found ${fromMsgs.length} messages total and ${msgs.length} new messages to migrate`); + event.reply('imap:migrate:progress', `- Found ${fromMsgs.length} messages total and ${msgs.length} new messages to migrate`); - await toClient.noop(); - for (const msg of msgs) { - appendCommands.push(toClient.append(fromFolder.path, msg.source, Array.from(msg.flags), msg.envelope.date)); + if (msgs.length > 0) { + event.reply('imap:migrate:progress', '- Starting migration'); + + await toClient.noop(); + for (const msg of msgs) { + appendCommands.push(toClient.append(fromFolder.path, msg.source, Array.from(msg.flags), msg.envelope.date)); + } + + await Promise.all(appendCommands); + event.reply('imap:migrate:progress', '- Done'); } - } finally { - await Promise.all(appendCommands); - event.reply('imap:migrate:progress', '- Done'); + fromMailbox.release(); toMailbox.release(); + } catch (e) { + event.reply('imap:migrate:error', e); } } diff --git a/app/index.html b/app/index.html index 8b7ddc9..df22432 100644 --- a/app/index.html +++ b/app/index.html @@ -2,8 +2,8 @@ <html> <head> <meta charset="UTF-8"> - <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"> - <title>Hello World!</title> + <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; img-src data:"> + <title>Mail Migrator</title> <link rel="stylesheet" href="./build/index.css"> </head> <body> diff --git a/app/src/components/Folders.vue b/app/src/components/Folders.vue index b801c1a..5c5c388 100644 --- a/app/src/components/Folders.vue +++ b/app/src/components/Folders.vue @@ -19,3 +19,10 @@ export default { }, }; </script> + +<style lang="scss"> +ul { + padding-left: 1em; + list-style-type: dot; +} +</style> diff --git a/app/src/components/Panel.vue b/app/src/components/Panel.vue index 527a8a9..eb92af7 100644 --- a/app/src/components/Panel.vue +++ b/app/src/components/Panel.vue @@ -1,5 +1,9 @@ <template> - <form data-abide @submit.prevent="$emit('connect')"> + <form @submit.prevent="connect"> + <div v-show="apiMessage.type" class="callout" :class="{ success: apiMessage.type === 'success', alert: apiMessage.type === 'error' }"> + <p>{{ apiMessage.msg }}</p> + </div> + <div class="grid-x"> <div class="cell"> <label> @@ -10,7 +14,14 @@ <div class="grid-x"> <div class="cell"> <label> - Port <input type="text" :value="modelValue.port" required @input="emit('port', $event.target.value)"> + Port + <Select + :model-value="modelValue.port" + required + :options="ports" + :config="{ tags: true }" + @update:modelValue="(value) => emit('port', value)" + /> </label> </div> </div> @@ -33,9 +44,14 @@ </template> <script> -import dynamicModelObjectEmit from '../mixins/dynamicModelObjectEmit'; +import Select from './Select'; +import dynamicModelObjectEmit from '~/mixins/dynamicModelObjectEmit'; export default { + components: { + Select, + }, + mixins: [ dynamicModelObjectEmit, ], @@ -45,8 +61,41 @@ export default { data () { return { uid: crypto.getRandomValues(new Uint8Array(1)), + apiMessage: { + type: '', + msg: '', + }, + + ports: [...new Set([ + 143, + 993, + Number(this.modelValue.port), + ])].map((port) => ({ id: port, text: port })), }; }, + + mounted () { + this.$electron.ipcRenderer.on('imap:connect:reply', (event, success) => { + this.apiMessage = { + type: 'success', + msg: 'Connected!', + }; + }); + + this.$electron.ipcRenderer.on('imap:connect:error', (event, error) => { + this.apiMessage = { + type: 'error', + msg: error, + }; + }); + }, + + methods: { + connect () { + this.$electron.ipcRenderer.send('imap:connect', JSON.parse(JSON.stringify(this.modelValue))); + this.$emit('connect'); + }, + }, }; </script> diff --git a/app/src/components/Select.vue b/app/src/components/Select.vue new file mode 100644 index 0000000..f2d1187 --- /dev/null +++ b/app/src/components/Select.vue @@ -0,0 +1,81 @@ +<template> + <select /> +</template> + +<script> +import 'select2/dist/js/select2.min'; +import $ from 'jquery'; + +export default { + props: { + options: { + type: Array, + required: true, + }, + + modelValue: { + type: [Number, String], + required: true, + }, + + config: { + type: Object, + default: () => ({}), + }, + }, + + emits: ['update:modelValue'], + + watch: { + options (value) { + $(this.$el) + .empty().select2({ data: value }); + }, + + modelValue (value) { + $(this.$el) + .val(value) + .trigger('change'); + }, + }, + + mounted () { + $(this.$el) + .select2({ ...this.config, data: this.options }) + .val(this.modelValue) + .trigger('change') + .on('change', (ev) => { + this.$emit('update:modelValue', ev.target.value); + }); + }, + + unmounted () { + $(this.$el) + .off() + .select2('destroy'); + }, +}; +</script> + +<style lang="scss"> +@import '~select2/dist/css/select2.min.css'; + +.select2 { + &-selection { + height: 2.4375rem !important; + border-radius: 0 !important; + border: 1px solid #cacaca !important; + + &__rendered { + padding: .5rem; + font-size: 1rem !important; + line-height: 1.5 !important; + } + + &__arrow { + height: 2.4375rem !important; + top: 0; + } + } +} +</style> diff --git a/app/src/pages/Home.vue b/app/src/pages/Home.vue deleted file mode 100644 index 8385a46..0000000 --- a/app/src/pages/Home.vue +++ /dev/null @@ -1,129 +0,0 @@ -<template> - <div class="wrap"> - <div class="panels"> - <div> - <h1>FROM</h1> - {{ from.error }} - <Panel v-model="from" @connect="connectFrom" /> - <ul v-if="from.folders"> - <Folders :folder="from.folders" /> - </ul> - </div> - - <div> - <h1>TO</h1> - {{ to.error }} - <Panel v-model="to" @connect="connectTo" /> - <ul v-if="to.folders"> - <Folders :folder="to.folders" /> - </ul> - </div> - </div> - - <button class="button" @click="migrate"> - Migrate! - </button> - - <div class="progress-screen"> - <b>{{ progress }}</b> - <br> - <div class="progress-screen__log"> - <div v-for="(msg, idx) in log" :key="idx"> - {{ msg }} - </div> - </div> - </div> - </div> -</template> - -<script> -import Panel from '../components/Panel.vue'; -import Folders from '../components/Folders'; - -export default { - components: { - Panel, - Folders, - }, - - data () { - return { - progress: '', - log: [], - }; - }, - - computed: { - from: { - set (value) { - this.$store.commit('setFrom', value); - }, - get () { - return this.$store.state.from; - }, - }, - to: { - set (value) { - this.$store.commit('setTo', value); - }, - get () { - return this.$store.state.to; - }, - }, - }, - - mounted () { - this.$electron.ipcRenderer.on('imap:listTree:from:reply', (event, folders) => { - this.from.folders = folders; - }); - - this.$electron.ipcRenderer.on('imap:listTree:to:reply', (event, folders) => { - this.to.folders = folders; - }); - - this.$electron.ipcRenderer.on('imap:from:error', (event, error) => { - this.from.error = error; - }); - this.$electron.ipcRenderer.on('imap:to:error', (event, error) => { - this.to.error = error; - }); - - this.$electron.ipcRenderer.on('imap:migrate:progress', (event, progress) => { - this.progress = progress; - this.log.push(progress); - }); - }, - - methods: { - connectFrom () { - this.$electron.ipcRenderer.send('imap:listTree:from', JSON.parse(JSON.stringify(this.from))); - }, - - connectTo () { - this.$electron.ipcRenderer.send('imap:listTree:to', JSON.parse(JSON.stringify(this.to))); - }, - - async migrate () { - this.progress = ''; - this.log = []; - - this.$electron.ipcRenderer.send('imap:migrate', { - from: JSON.parse(JSON.stringify(this.from)), - to: JSON.parse(JSON.stringify(this.to)), - }); - }, - }, -}; -</script> - -<style lang="scss"> -.panels { - display: flex; - justify-content: space-between; -} - -ul { - padding-left: 1em; - list-style-type: dot; -} -</style> diff --git a/app/src/pages/Steps/1-From.vue b/app/src/pages/Steps/1-From.vue new file mode 100644 index 0000000..e1bd82e --- /dev/null +++ b/app/src/pages/Steps/1-From.vue @@ -0,0 +1,32 @@ +<template> + <div> + <h1>From</h1> + + <Panel v-model="from" /> + + <router-link to="/steps/2"> + next + </router-link> + </div> +</template> + +<script> +import Panel from '~/components/Panel'; + +export default { + components: { + Panel, + }, + + computed: { + from: { + set (value) { + this.$store.commit('setFrom', value); + }, + get () { + return this.$store.state.from; + }, + }, + }, +}; +</script> diff --git a/app/src/pages/Steps/2-To.vue b/app/src/pages/Steps/2-To.vue new file mode 100644 index 0000000..1cce01d --- /dev/null +++ b/app/src/pages/Steps/2-To.vue @@ -0,0 +1,51 @@ +<template> + <div> + <router-link to="/steps/1"> + previous + </router-link> + + <h1>To</h1> + + <Panel v-model="to" @connect="connect" /> + + <router-link to="/steps/3"> + next + </router-link> + </div> +</template> + +<script>import Panel from '~/components/Panel'; + +export default { + components: { + Panel, + }, + + computed: { + to: { + set (value) { + this.$store.commit('setTo', value); + }, + get () { + return this.$store.state.to; + }, + }, + }, + + mounted () { + this.$electron.ipcRenderer.on('imap:listTree:to:reply', (event, folders) => { + this.to.folders = folders; + }); + + this.$electron.ipcRenderer.on('imap:to:error', (event, error) => { + this.to.error = error; + }); + }, + + methods: { + connect () { + this.$electron.ipcRenderer.send('imap:listTree:to', JSON.parse(JSON.stringify(this.to))); + }, + }, +}; +</script> diff --git a/app/src/pages/Steps/3-Migrate.vue b/app/src/pages/Steps/3-Migrate.vue new file mode 100644 index 0000000..c8d2e39 --- /dev/null +++ b/app/src/pages/Steps/3-Migrate.vue @@ -0,0 +1,111 @@ +<template> + <div> + <router-link to="/steps/2"> + previous + </router-link> + + <div> + From + <div class="input-group"> + <input type="text" disabled :value="from.server"> <input type="text" disabled :value="from.port"> + </div> + </div> + <div> + To + <div class="input-group"> + <input type="text" disabled :value="to.server"> <input type="text" disabled :value="to.port"> + </div> + </div> + + <div> + Bedingungen + <br> + <input type="text"> + </div> + + <ul v-if="from.folders"> + <Folders :folder="from.folders" /> + </ul> + + <button class="button" @click="migrate"> + Migrate! + </button> + + <div class="progress-screen"> + <b>{{ progress }}</b> + <br> + <b>{{ progressMessages }}</b> + <br> + <div class="progress-screen__log"> + <div v-for="(msg, idx) in log" :key="idx"> + {{ msg }} + </div> + </div> + </div> + </div> +</template> + +<script> +import Folders from '~/components/Folders'; + +export default { + components: { + Folders, + }, + + data () { + return { + progress: '', + log: [], + progressMessages: '', + }; + }, + + computed: { + from: { + set (value) { + this.$store.commit('setFrom', value); + }, + get () { + return this.$store.state.from; + }, + }, + to: { + set (value) { + this.$store.commit('setTo', value); + }, + get () { + return this.$store.state.to; + }, + }, + }, + + mounted () { + this.$electron.ipcRenderer.on('imap:listTree:from:reply', (event, folders) => { + this.from.folders = folders; + }); + this.$electron.ipcRenderer.send('imap:listTree:from', JSON.parse(JSON.stringify(this.from))); + + this.$electron.ipcRenderer.on('imap:migrate:progress', (event, progress) => { + this.progress = progress; + this.log.push(progress); + }); + this.$electron.ipcRenderer.on('imap:migrate:progress:messages', (event, progress) => { + this.progressMessages = progress; + }); + }, + + methods: { + async migrate () { + this.progress = ''; + this.log = []; + this.progressMessages = ''; + + this.$electron.ipcRenderer.send('imap:migrate', JSON.parse(JSON.stringify({ + from: this.from, + to: this.to, + }))); + }, + }, +}; +</script> diff --git a/app/src/router.js b/app/src/router.js index 7676449..93ca201 100644 --- a/app/src/router.js +++ b/app/src/router.js @@ -1,12 +1,26 @@ import { createRouter, createWebHashHistory } from 'vue-router'; -import Home from './pages/Home'; +import Step1 from './pages/Steps/1-From'; +import Step2 from './pages/Steps/2-To'; +import Step3 from './pages/Steps/3-Migrate'; export default createRouter({ history: createWebHashHistory(), routes: [ { path: '/', - component: Home, + redirect: '/steps/1', + }, + { + path: '/steps/1', + component: Step1, + }, + { + path: '/steps/2', + component: Step2, + }, + { + path: '/steps/3', + component: Step3, }, ], }); diff --git a/package-lock.json b/package-lock.json index cc1cdf1..9f48f98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6726,6 +6726,11 @@ } } }, + "select2": { + "version": "4.1.0-rc.0", + "resolved": "https://registry.npmjs.org/select2/-/select2-4.1.0-rc.0.tgz", + "integrity": "sha512-Hr9TdhyHCZUtwznEH2CBf7967mEM0idtJ5nMtjvk3Up5tPukOLXbHUNmh10oRfeNIhj+3GD3niu+g6sVK+gK0A==" + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", diff --git a/package.json b/package.json index d78b675..6d9a7c9 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "foundation-sites": "^6.6.3", "imapflow": "^1.0.59", "jquery": "^3.6.0", + "select2": "^4.1.0-rc.0", "vue": "^3.1.1", "vue-router": "^4.0.10", "vuex": "^4.0.2" |