From 1fb130988a734bd7ece8956d46254f706bf5532c Mon Sep 17 00:00:00 2001 From: NGPixel Date: Tue, 1 Aug 2023 21:52:55 +0000 Subject: [PATCH] feat: upload pending assets --- server/locales/en.json | 1 + ux/public/_assets/icons/fluent-upload.svg | 1 + .../_assets/illustrations/undraw_upload.svg | 1 + ux/src/components/EditorMarkdown.vue | 51 ++++++-- ux/src/components/PageActionsCol.vue | 2 +- ux/src/components/PageHeader.vue | 15 +++ .../components/UploadPendingAssetsDialog.vue | 115 ++++++++++++++++++ ux/src/stores/editor.js | 33 +++++ 8 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 ux/public/_assets/icons/fluent-upload.svg create mode 100644 ux/public/_assets/illustrations/undraw_upload.svg create mode 100644 ux/src/components/UploadPendingAssetsDialog.vue diff --git a/server/locales/en.json b/server/locales/en.json index 6d7ab89c..f9847297 100644 --- a/server/locales/en.json +++ b/server/locales/en.json @@ -1530,6 +1530,7 @@ "editor.pageRel.title": "Add Page Relation", "editor.pageRel.titleEdit": "Edit Page Relation", "editor.pageScripts.title": "Page Scripts", + "editor.pendingAssetsUploading": "Uploading assets...", "editor.props.alias": "Alias", "editor.props.allowComments": "Allow Comments", "editor.props.allowCommentsHint": "Enable commenting abilities on this page.", diff --git a/ux/public/_assets/icons/fluent-upload.svg b/ux/public/_assets/icons/fluent-upload.svg new file mode 100644 index 00000000..0c4d998d --- /dev/null +++ b/ux/public/_assets/icons/fluent-upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ux/public/_assets/illustrations/undraw_upload.svg b/ux/public/_assets/illustrations/undraw_upload.svg new file mode 100644 index 00000000..b54240d4 --- /dev/null +++ b/ux/public/_assets/illustrations/undraw_upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ux/src/components/EditorMarkdown.vue b/ux/src/components/EditorMarkdown.vue index 104760db..94549d1c 100644 --- a/ux/src/components/EditorMarkdown.vue +++ b/ux/src/components/EditorMarkdown.vue @@ -28,6 +28,8 @@ q-item-label From File Manager... q-item( clickable + @click='getAssetFromClipboard' + v-close-popup ) q-item-section(side) q-icon(name='las la-clipboard', color='brown') @@ -251,11 +253,10 @@ import { reactive, ref, shallowRef, nextTick, onMounted, watch, onBeforeUnmount } from 'vue' import { useMeta, useQuasar, setCssVar } from 'quasar' import { useI18n } from 'vue-i18n' -import { get, flatten, last, times, startsWith, debounce } from 'lodash-es' +import { find, get, last, times, startsWith, debounce } from 'lodash-es' import { DateTime } from 'luxon' import * as monaco from 'monaco-editor' import { Position, Range } from 'monaco-editor' -import { v4 as uuid } from 'uuid' import { useEditorStore } from 'src/stores/editor' import { usePageStore } from 'src/stores/page' @@ -477,6 +478,43 @@ function openEditorSettings () { siteStore.$patch({ overlay: 'EditorMarkdownConfig' }) } +async function getAssetFromClipboard () { + try { + const permission = await navigator.permissions.query({ + name: 'clipboard-read' + }) + if (permission.state === 'denied') { + throw new Error('Not allowed to read clipboard.') + } + const clipboardContents = await navigator.clipboard.read() + let hasValidItem = false + for (const item of clipboardContents) { + const imageType = find(item.types, t => t.startsWith('image/')) + if (imageType) { + hasValidItem = true + const blob = await item.getType(imageType) + const blobUrl = editorStore.addPendingAsset(blob) + insertAtCursor({ + content: `![](${blobUrl})` + }) + } + } + if (!hasValidItem) { + throw new Error('No supported content found in the Clipboard.') + } + } catch (err) { + return $q.notify({ + type: 'negative', + message: 'Unable to copy from Clipboard', + caption: err.message + }) + } +} + +function reloadEditorContent () { + editor.getModel().setValue(pageStore.content) +} + // MOUNTED onMounted(async () => { @@ -624,12 +662,7 @@ onMounted(async () => { editor.getContainerDomNode().addEventListener('drop', ev => { ev.preventDefault() for (const file of ev.dataTransfer.files) { - const blobUrl = URL.createObjectURL(file) - editorStore.pendingAssets.push({ - id: uuid(), - file, - blobUrl - }) + const blobUrl = editorStore.addPendingAsset(file) if (file.type.startsWith('image')) { insertAtCursor({ content: `![${file.name}](${blobUrl})` @@ -652,6 +685,7 @@ onMounted(async () => { EVENT_BUS.on('insertAsset', insertAssetClb) EVENT_BUS.on('openEditorSettings', openEditorSettings) + EVENT_BUS.on('reloadEditorContent', reloadEditorContent) // this.$root.$on('editorInsert', opts => { // switch (opts.kind) { @@ -689,6 +723,7 @@ onMounted(async () => { onBeforeUnmount(() => { EVENT_BUS.off('insertAsset', insertAssetClb) EVENT_BUS.off('openEditorSettings', openEditorSettings) + EVENT_BUS.off('reloadEditorContent', reloadEditorContent) if (editor) { editor.dispose() } diff --git a/ux/src/components/PageActionsCol.vue b/ux/src/components/PageActionsCol.vue index 2da9962a..668baebe 100644 --- a/ux/src/components/PageActionsCol.vue +++ b/ux/src/components/PageActionsCol.vue @@ -51,7 +51,7 @@ q-item(v-for='item of editorStore.pendingAssets') q-item-section(side) q-icon(name='las la-file-image') - q-item-section {{ item.file.name }} + q-item-section {{ item.fileName }} q-item-section(side) q-btn.acrylic-btn( color='negative' diff --git a/ux/src/components/PageHeader.vue b/ux/src/components/PageHeader.vue index 587c73dc..84569fd2 100644 --- a/ux/src/components/PageHeader.vue +++ b/ux/src/components/PageHeader.vue @@ -289,6 +289,7 @@ async function saveChanges (closeAfter = false) { } async function saveChangesCommit (closeAfter = false) { + await processPendingAssets() $q.loading.show() try { await pageStore.pageSave() @@ -315,6 +316,7 @@ async function saveChangesCommit (closeAfter = false) { async function createPage () { // Handle home page creation flow if (pageStore.path === 'home') { + await processPendingAssets() $q.loading.show() try { await pageStore.pageSave() @@ -347,6 +349,8 @@ async function createPage () { itemFileName: pageStore.path } }).onOk(async ({ path, title }) => { + await processPendingAssets() + $q.loading.show() try { pageStore.$patch({ @@ -372,6 +376,17 @@ async function createPage () { }) } +async function processPendingAssets () { + if (editorStore.pendingAssets?.length > 0) { + return new Promise((resolve, reject) => { + $q.dialog({ + component: defineAsyncComponent(() => import('../components/UploadPendingAssetsDialog.vue')), + persistent: true + }).onOk(resolve).onCancel(reject) + }) + } +} + async function editPage () { $q.loading.show() await pageStore.pageEdit() diff --git a/ux/src/components/UploadPendingAssetsDialog.vue b/ux/src/components/UploadPendingAssetsDialog.vue new file mode 100644 index 00000000..d45b44d9 --- /dev/null +++ b/ux/src/components/UploadPendingAssetsDialog.vue @@ -0,0 +1,115 @@ + + + diff --git a/ux/src/stores/editor.js b/ux/src/stores/editor.js index fff6aca4..7374a0ea 100644 --- a/ux/src/stores/editor.js +++ b/ux/src/stores/editor.js @@ -1,9 +1,19 @@ import { defineStore } from 'pinia' import gql from 'graphql-tag' import { clone } from 'lodash-es' +import { v4 as uuid } from 'uuid' import { useSiteStore } from './site' +const imgMimeExt = { + 'image/jpeg': 'jpg', + 'image/gif': 'gif', + 'image/png': 'png', + 'image/webp': 'webp', + 'image/svg+xml': 'svg', + 'image/tiff': 'tif' +} + export const useEditorStore = defineStore('editor', { state: () => ({ isActive: false, @@ -33,6 +43,29 @@ export const useEditorStore = defineStore('editor', { } }, actions: { + addPendingAsset (data) { + const blobUrl = URL.createObjectURL(data) + if (data instanceof File) { + this.pendingAssets.push({ + id: uuid(), + kind: 'file', + file: data, + fileName: data.name, + blobUrl + }) + } else { + const fileId = uuid() + const fileName = `${fileId}.${imgMimeExt[data.type] || 'dat'}` + this.pendingAssets.push({ + id: fileId, + kind: 'blob', + file: new File(data, fileName, { type: data.type }), + fileName, + blobUrl + }) + } + return blobUrl + }, async fetchConfigs () { const siteStore = useSiteStore() try {