diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml deleted file mode 100644 index 70ffe2b..0000000 --- a/.github/workflows/deploy-pages.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Deploy static content to Pages - -on: - push: - branches: [$default-branch] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '>=18' - - - name: Defining environment variables - run: | - echo "VITE_SERVER_URL=https://server.json.ms" > .env - echo "VITE_DEMO_PREVIEW_URL=https://demo.json.ms" >> .env - - - name: Install dependencies - run: yarn - - - name: Build project - run: yarn build-only - - - name: Copy index.html to 404.html - run: cp ./dist/index.html ./dist/404.html - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./dist - - # Deployment job - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/package.json b/package.json index 26e499c..78af4ee 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@json.ms/www", "private": true, "type": "module", - "version": "1.2.18", + "version": "1.2.19", "scripts": { "dev": "vite --host", "build": "run-p type-check \"build-only {@}\" --", diff --git a/src/components/ActionBar.vue b/src/components/ActionBar.vue index f287ab8..51af7a5 100644 --- a/src/components/ActionBar.vue +++ b/src/components/ActionBar.vue @@ -4,7 +4,7 @@ import {useStructure} from '@/composables/structure'; import {useLayout} from '@/composables/layout'; import {useGlobalStore} from '@/stores/global'; import {computed} from "vue"; -import {useTypings} from "@/composables/typings"; +import {useSyncing} from "@/composables/syncing"; import {useModelStore} from "@/stores/model"; import TriggerMenu from "@/components/TriggerMenu.vue"; import type {IStructure, IStructureData} from "@/interfaces"; @@ -45,7 +45,7 @@ const onSetAsDefaultValues = () => { } const onSyncWithLocalOnly = () => { - useTypings().syncToFolder(modelStore.structure, 'typescript', ['data']); + useSyncing().syncToFolder(modelStore.structure, ['data']); userDataSaved.value = true; setTimeout(() => userDataSaved.value = false, 1000); } diff --git a/src/components/FileFieldItem.vue b/src/components/FileFieldItem.vue index ee5dcb5..1137021 100644 --- a/src/components/FileFieldItem.vue +++ b/src/components/FileFieldItem.vue @@ -6,6 +6,7 @@ import {useDisplay} from "vuetify"; import {useGlobalStore} from "@/stores/global"; import ImgTag from "@/components/ImgTag.vue"; import VideoPlayer from "@/components/VideoPlayer.vue"; +import {blobFileList} from "@/composables/syncing"; const value = defineModel({ required: true }); const { @@ -40,12 +41,15 @@ const thumbnailSize = (file: IFile): { width: number, height: number } => { }; } const isImage = (file: IFile): boolean => { - return file.meta.type.startsWith('image/'); + return file.meta.type?.startsWith('image/') || false; } const isVideo = (file: IFile): boolean => { - return file.meta.type.startsWith('video/'); + return file.meta.type?.startsWith('video/') || false; } const src = computed((): string => { + if (file.path && blobFileList[file.path]) { + return blobFileList[file.path]; + } if (file.path && (file.path.startsWith('http://') || file.path.startsWith('https://'))) { return file.path; } @@ -60,7 +64,7 @@ const onDownloadFile = (file: IFile) => { .then(blob => { const link = document.createElement('a'); link.href = URL.createObjectURL(blob); - link.download = file.meta.originalFileName; + link.download = file.meta.originalFileName || 'unknown'; document.body.appendChild(link); link.click(); document.body.removeChild(link); @@ -155,12 +159,11 @@ const onRemoveFile = (file: IFile) => { v-bind="props" :disabled="disabled" :size="smAndDown ? 'small' : 'default'" - color="error" variant="text" icon @click="() => onRemoveFile(file)" > - + diff --git a/src/components/FileManager.vue b/src/components/FileManager.vue index e3ab295..8b05516 100644 --- a/src/components/FileManager.vue +++ b/src/components/FileManager.vue @@ -7,12 +7,15 @@ import ImgTag from '@/components/ImgTag.vue'; import VideoPlayer from '@/components/VideoPlayer.vue'; import {downloadFilesAsZip, getFileIcon, phpStringSizeToBytes} from '@/utils'; import ModalDialog from '@/components/ModalDialog.vue'; +import {isFolderSynced, useSyncing, blobFileList} from "@/composables/syncing"; const globalStore = useGlobalStore(); +const syncing = useSyncing(); const structure = defineModel({ required: true }); -const { selected = [], serverSettings, canUpload = false, canDelete = false, canSelect = false, canDownload = false } = defineProps<{ +const { selected = [], serverSettings, canUpload = false, canAddToLocal = false, canDelete = false, canSelect = false, canDownload = false } = defineProps<{ selected?: IFile[], canUpload?: boolean, + canAddToLocal?: boolean, canDelete?: boolean, canSelect?: boolean, canDownload?: boolean, @@ -105,68 +108,116 @@ const onFileClick = (file: IFile) => { } const load = () => { - if (structure.value.endpoint) { - loading.value = true; - return Services.get(structure.value.server_url + '/file/list/' + structure.value.hash, { + loading.value = true; + Promise.all([ + isFolderSynced(structure.value) ? syncing.getFiles(structure.value) : [], + structure.value.endpoint ? Services.get(structure.value.server_url + '/file/list/' + structure.value.hash, { 'Content-Type': 'application/json', 'X-Jms-Api-Key': structure.value.server_secret, - }) - .then(response => files.value = response) - .then(() => { - selectedFiles.value = []; - selected.forEach(selectedFile => { - const file = files.value.find(file => file.path === selectedFile.path); - if (file) { - selectedFiles.value.push(file); + }) : [] + ]).then(([syncResponse, endpointResponse]) => { + files.value = []; + syncResponse.forEach(item => { + if (!['data.json', 'structure.json', 'structure.yml', 'default.ts', 'index.ts', 'typings.ts', 'settings.json'].includes(item.path)) { + files.value.push({ + path: item.path, + meta: { + type: item.file.type, + width: item.width, + height: item.height, + size: item.file.size, + timestamp: item.file.lastModified / 1000, + originalFileName: item.file.name, } - }) - }) - .catch(globalStore.catchError) - .finally(() => loading.value = false); - } + }); + } + }); + + endpointResponse.forEach((item: IFile) => { + files.value.push(item); + }); + + selectedFiles.value = []; + selected.forEach(selectedFile => { + const file = files.value.find(file => file.path === selectedFile.path); + if (file) { + selectedFiles.value.push(file); + } + }) + }) + .catch(globalStore.catchError) + .finally(() => loading.value = false); } -const promptUpload = () => { +const promptUpload = (type: 'remote' | 'local' | null = null) => { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.multiple = true; if (globalStore.fileManager.accept) { fileInput.accept = globalStore.fileManager.accept; } + if (type === null && canAddToLocal) { + type = 'local'; + } + if (type === null && canUpload) { + type = 'remote'; + } fileInput.addEventListener('change', function() { - if (fileInput.files && fileInput.files.length > 0) { - upload(fileInput.files); + if (fileInput.files && fileInput.files.length > 0 && type) { + upload(fileInput.files, type); } }); fileInput.click(); } -const upload = async (fileList: FileList) => { +const upload = async (fileList: FileList, type: 'remote' | 'local') => { uploading.value = true; uploadProgress.value = 0; const promises = []; - for (let i = 0; i < fileList.length; i++) { - const file = fileList[i]; - if (file.size > phpStringSizeToBytes(serverSettings.uploadMaxSize)) { - globalStore.catchError(new Error( - 'This file is exceeding the maximum size of ' + serverSettings.uploadMaxSize + ' defined by the server.' - )); + if (type === 'remote') { + for (let i = 0; i < fileList.length; i++) { + const file = fileList[i]; + if (file.size > phpStringSizeToBytes(serverSettings.uploadMaxSize)) { + globalStore.catchError(new Error( + 'This file is exceeding the maximum size of ' + serverSettings.uploadMaxSize + ' defined by the server.' + )); + } } } for (let i = 0; i < fileList.length; i++) { const file = fileList[i]; - promises.push( - Services.upload(structure.value.server_url + '/file/upload/' + structure.value.hash, file, progress => uploadProgress.value = progress, { - 'X-Jms-Api-Key': structure.value.server_secret, - }) - .then(response => { - if (!files.value.find(item => item.path === response.internalPath)) { - files.value.push({ - 'path': response.internalPath, - 'meta': response.meta, - }) - } + if (type === 'remote') { + promises.push( + Services.upload(structure.value.server_url + '/file/upload/' + structure.value.hash, file, progress => uploadProgress.value = progress, { + 'X-Jms-Api-Key': structure.value.server_secret, + }) + .then(response => { + if (!files.value.find(item => item.path === response.internalPath)) { + files.value.push({ + 'path': response.internalPath, + 'meta': response.meta, + }) + } + })) + } else if (type === 'local') { + const folder = 'files'; + promises.push(syncing.addFile(structure.value, file, folder).then(async () => { + const path = folder + '/' + file.name; + const metadata = await syncing.getFileMetadata(file, path); + syncing.loadBlob(file, path) + files.value.push({ + path, + meta: { + type: file.type, + width: metadata.width, + height: metadata.height, + size: file.size, + timestamp: file.lastModified / 1000, + originalFileName: file.name, + } + }); })) + } } return Promise.all(promises) .catch(globalStore.catchError) @@ -187,23 +238,33 @@ const remove = () => { const promises = []; for (let i = 0; i < selectedFiles.value.length; i++) { const file = selectedFiles.value[i]; - promises.push( - Services.delete(structure.value.server_url + '/file/delete/' + structure.value.hash + '/' + file.path, { - 'X-Jms-Api-Key': structure.value.server_secret, - }) - .then(() => { - selectedFiles.value = selectedFiles.value.filter(item => item.path !== file.path); - files.value = files.value.filter(item => item.path !== file.path); + const removeCallback = () => { + selectedFiles.value = selectedFiles.value.filter(item => item.path !== file.path); + files.value = files.value.filter(item => item.path !== file.path); + } + if (file.path && blobFileList[file.path]) { + promises.push( + syncing.removeFile(structure.value, file.path) + .then(removeCallback) + ); + } else { + promises.push( + Services.delete(structure.value.server_url + '/file/delete/' + structure.value.hash + '/' + file.path, { + 'X-Jms-Api-Key': structure.value.server_secret, }) - ); + .then(removeCallback) + ); + } } return Promise.all(promises) .catch(err => { reject(); globalStore.catchError(err); }) - .finally(resolve) - .finally(() => deleting.value = false); + .finally(() => { + resolve(); + deleting.value = false; + }) }) }); } @@ -220,7 +281,7 @@ const select = () => { const download = () => { downloading.value = true; nextTick(() => { - downloadFilesAsZip(selectedFiles.value.map(item => serverSettings.publicUrl + item.path), false, 'jsonms-file-download.zip', globalStore.userSettings.data.editorTabSize); + downloadFilesAsZip(selectedFiles.value.map(item => (item.path && blobFileList[item.path]) || (serverSettings.publicUrl + item.path)), false, 'jsonms-file-download.zip', globalStore.userSettings.data.editorTabSize); downloading.value = false; }) } @@ -334,7 +395,7 @@ watch(() => globalStore.fileManager.visible, () => { @dragover.prevent.stop="onDragEnter" @dragleave.prevent.stop="onDragLeave" > -
+
globalStore.fileManager.visible, () => { flat > + globalStore.fileManager.visible, () => {
@@ -440,6 +507,17 @@ watch(() => globalStore.fileManager.visible, () => {
+ globalStore.fileManager.visible, () => { text="Upload" variant="outlined" class="px-3" - @click="promptUpload" + @click="() => promptUpload('remote')" />