diff --git a/electron-builder.conf.js b/electron-builder.conf.js index 5bf93b249ff19b9fa9f196ec800a0d17699a3583..beacc054f0807a0e4a12e7701ca1cf03a77880fa 100644 --- a/electron-builder.conf.js +++ b/electron-builder.conf.js @@ -58,15 +58,26 @@ module.exports = { }; fs.writeFileSync('dist/appInfo.json', JSON.stringify(appInfo, null, 2)); }, - fileAssociations: { - ext: 'epoc', - name: 'ePoc Project', - description: 'ePoc project content package', - mimeType: 'application/zip', - role: 'Editor', - isPackage: false, - rank: 'Default' - }, + fileAssociations: [ + { + ext: 'epoc', + name: 'ePoc', + description: 'ePoc publication', + mimeType: 'application/zip', + role: 'Mobile Application', + isPackage: false, + rank: 'Default' + }, + { + ext: 'epocproject', + name: 'ePoc Project', + description: 'ePoc project content package', + mimeType: 'application/zip', + role: 'Editor', + isPackage: false, + rank: 'Default' + } + ], afterSign: async (context) => { const { electronPlatformName, appOutDir } = context; if (electronPlatformName !== 'darwin' || process.env.NO_NOTARIZE) { diff --git a/electron/components/file.js b/electron/components/file.js index deef92381b3c242c0b14d957aced53d85b0cbc7b..8663ae8a322718201bae5b6315d660d89ded7748 100644 --- a/electron/components/file.js +++ b/electron/components/file.js @@ -62,11 +62,23 @@ const pickEpocToImport = async function () { const startTime = performance.now(); const files = dialog.showOpenDialogSync(BrowserWindow.getFocusedWindow(), { properties: ['openFile'], - filters: [{ name: 'ePoc export archive', extensions: ['zip'] }], + filters: [{ name: 'ePoc export archive', extensions: ['zip', 'epoc'] }], }); if (!files || !files[0]) return null; const filepath = files[0]; + const workdir = createWorkDir(); + return importEpoc(filepath, startTime, workdir); +}; + +/** + * Import an ePoc project to the workdir + * @param {string} filepath - the path to the ePoc + * @param {number} startTime - the time when the import started + * @param {string} workdir - the path to the workdir + * @returns {{filepath: null, workdir: string, name: null, modified: Date} | null} + */ +const importEpoc = async function(filepath, startTime, workdir) { const zip = new AdmZip(filepath, {}); zip.extractAllTo(workdir, true, false, null); @@ -93,7 +105,7 @@ const pickEpocToImport = async function () { const pickEpocProject = function () { const files = dialog.showOpenDialogSync(BrowserWindow.getFocusedWindow(), { properties: ['openFile'], - filters: [{ name: 'ePoc', extensions: ['epoc'] }], + filters: [{ name: 'ePoc', extensions: ['epocproject', 'epoc'] }], }); if (!files || !files[0]) return null; @@ -107,19 +119,29 @@ const pickEpocProject = function () { /** * Unzip the content of an ePoc project file to the project workdir - * @returns {{filepath: string, workdir: null, name: null, modified: Date}} + * @returns {{project: {filepath: string, workdir: null, name: null, modified: Date} | null, import: boolean} | null} */ const openEpocProject = async function (filepath) { if (!filepath) return null; const startTime = performance.now(); + const workdir = createWorkDir(); const zip = new AdmZip(filepath, {}); try { zip.extractAllTo(workdir, true, false, null); + if(!fs.existsSync(path.join(workdir, 'project.json'))) { + const project = await importEpoc(filepath, startTime, workdir); + return { + project, + imported: true + }; + } } catch (err) { return null; } + console.log('continuing the function'); + const project = { name: path.basename(filepath), modified: fs.statSync(filepath).mtime, @@ -132,11 +154,14 @@ const openEpocProject = async function (filepath) { const ellapsed = performance.now() - startTime; if (ellapsed < 500) await wait(500 - ellapsed); - return project; + return { + project, + imported: false, + }; }; /** - * Save the content of the ePoc project workdir to the currently opened .epoc file or call for saveAs + * Save the content of the ePoc project workdir to the currently opened .epocproject file or call for saveAs * @returns {string} */ const saveEpocProject = async function (project) { @@ -148,12 +173,12 @@ const saveEpocProject = async function (project) { }; /** - * Save the content of the ePoc project workdir to a new .epoc file + * Save the content of the ePoc project workdir to a new .epocproject file * @returns {string} */ const saveAsEpocProject = async function (project) { const files = dialog.showSaveDialogSync(BrowserWindow.getFocusedWindow(), { - filters: [{ name: 'ePoc', extensions: ['epoc'] }], + filters: [{ name: 'ePoc', extensions: ['epocproject'] }], }); if (!files) return null; @@ -166,7 +191,7 @@ const saveAsEpocProject = async function (project) { /** * Zip files of an ePoc project * @param {string} workdir the path to the workdir - * @param {string} filepath the path to the .epoc project file + * @param {string} filepath the path to the .epocproject file * @param {boolean} exporting if true, the project.json file will not be included */ const zipFiles = async function (workdir, filepath, exporting) { @@ -198,19 +223,19 @@ const zipEpocProject = async function (workdir, filepath) { }; /** - * Export the content of an ePoc next to the .epoc file + * Export the content of an ePoc next to the .epocproject file * @param {string} workdir the path to the workdir - * @param {string} filepath the path to the .epoc project file + * @param {string} filepath the path to the .epocproject file * @return {string|null} */ const exportProject = async function (workdir, filepath) { const defaultPath = filepath - ? path.join(path.dirname(filepath), path.basename(filepath, path.extname(filepath)) + '.zip') + ? path.join(path.dirname(filepath), path.basename(filepath, path.extname(filepath)) + '.epoc') : ''; const exportPath = dialog.showSaveDialogSync(BrowserWindow.getFocusedWindow(), { defaultPath: defaultPath, - filters: [{ name: 'zip', extensions: ['zip'] }], + filters: [{ name: 'epoc', extensions: ['epoc'] }], }); if (!exportPath) return null; diff --git a/electron/components/ipc.js b/electron/components/ipc.js index 6f686072e473c140996f90cffb9124a0ef6e8cab..1ace93ee223c9c703caa83710fda121157ba2af0 100644 --- a/electron/components/ipc.js +++ b/electron/components/ipc.js @@ -52,15 +52,21 @@ const setupIpcListener = function (targetWindow) { ipcMain.on('openEpocProject', async (event, epocProjectPath) => { if (event.sender !== targetWindow.webContents) return; - const project = await openEpocProject(epocProjectPath); + const { project, imported } = await openEpocProject(epocProjectPath); if (!project) { sendToFrontend(event.sender, 'epocProjectError'); return; } - const flow = await readProjectData(project.workdir); - sendToFrontend(event.sender, 'epocProjectReady', { project, flow }); - store.updateState('projects', { [targetWindow.id]: project }); + + if(imported) { + sendToFrontend(event.sender, 'importRequired', project); + } else { + const flow = await readProjectData(project.workdir); + sendToFrontend(event.sender, 'epocProjectReady', { project, flow }); + store.updateState('projects', { [targetWindow.id]: project }); + } + }); ipcMain.on('newEpocProject', async (event) => { diff --git a/electron/components/menu.js b/electron/components/menu.js index 1b9a22ed08a042cffa472922f2b3f34178ee4806..323ccaa6563bd893ad13a2fa8cb4b7591333ea39 100644 --- a/electron/components/menu.js +++ b/electron/components/menu.js @@ -59,7 +59,7 @@ module.exports.setupMenu = function () { ], }, { - label: 'Importer un fichier zip', + label: 'Importer un fichier .epoc', click: async function () { sendToFrontend(BrowserWindow.getFocusedWindow(), 'epocImportPicked'); const project = await pickEpocToImport(); diff --git a/src/components/ChoiceModal.vue b/src/components/ChoiceModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..0202008aa448f6ed7d706a0579e43b05baa96d1d --- /dev/null +++ b/src/components/ChoiceModal.vue @@ -0,0 +1,98 @@ +<script setup lang="ts"> +import { ref, onMounted } from 'vue'; + +interface Props { + acceptLabel: string; + cancelLabel: string; +} + +withDefaults(defineProps<Props>(), { + acceptLabel: 'Accepter', + cancelLabel: 'Annuler', +}); + +const emits = defineEmits<{ + (e: 'accept'): void; + (e: 'cancel'): void; +}>(); + +const modalScreen = ref<HTMLElement | null>(null); + +function validate() { + emits('accept'); +} + +function cancel() { + emits('cancel'); +} + +onMounted(() => { + modalScreen.value.focus(); +}); +</script> + +<template> + <div + ref="modalScreen" + class="modal-backdrop" + tabindex="0" + @keyup.enter="validate" + @keyup.esc="cancel" + > + <div class="modal"> + <slot /> + <button class="btn btn-close" @click="cancel"><i class="icon-x"></i></button> + <button class="btn-choice accept" @click="validate">{{ acceptLabel }}</button> + <button class="btn-choice cancel" @click="cancel">{{ cancelLabel }}</button> + </div> + </div> +</template> + +<style lang="scss" scoped> +.modal-backdrop { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: rgba(0, 0, 0, 0.3); + display: flex; + justify-content: center; + align-items: center; + z-index: 500; +} + +.modal { + user-select: none; + position: relative; + padding: 2rem; + background-color: white; + display: flex; + flex-direction: column; + border-radius: 10px; + gap: 1rem; + width: 22rem; +} + +.btn-choice { + cursor: pointer; + border: none; + border-radius: 30px; + font-size: 1rem; + font-weight: 500; + padding: 1rem; + + &:hover { + filter: brightness(95%); + } + &.cancel { + background-color: #fff; + color: var(--inria-grey); + border: 1px solid var(--inria-grey); + } + &.accept { + background-color: #e93100; + color: #fff; + } +} +</style> \ No newline at end of file diff --git a/src/components/ValidationModal.vue b/src/components/ValidationModal.vue index 368e89ceee9d02c57a0a6ca5632612846b4291f6..11a0db7e67722c5181942e9601a88bad7194ac3f 100644 --- a/src/components/ValidationModal.vue +++ b/src/components/ValidationModal.vue @@ -1,17 +1,11 @@ <script setup lang="ts"> -import { onMounted, ref } from 'vue'; -import { useEditorStore } from '../shared/stores'; -import { deleteSelectedNodes } from '../shared/services/graph'; -import { saveState } from '../shared/services/undoRedo.service'; +import { useEditorStore } from '@/src/shared/stores'; +import { deleteSelectedNodes } from '@/src/shared/services/graph'; +import { saveState } from '@/src/shared/services/undoRedo.service'; +import ChoiceModal from '@/src/components/ChoiceModal.vue'; const editorStore = useEditorStore(); -const modalScreen = ref(null); - -onMounted(() => { - modalScreen.value.focus(); -}); - function confirmDelete() { saveState(); deleteSelectedNodes(); @@ -19,73 +13,18 @@ function confirmDelete() { </script> <template> - <div - ref="modalScreen" - class="modal-backdrop" - tabindex="0" - @keyup.enter="confirmDelete" - @keyup.esc="editorStore.validationModal = false" + <ChoiceModal + accept-label="OUI, SUPPRIMER" + cancel-label="NON, NE PAS SUPPRIMER" + @cancel="editorStore.validationModal = false" + @accept="confirmDelete" > - <div class="modal"> - <h3>Souhaitez-vous vraiment supprimer cet élément ?</h3> - <button class="btn btn-close" @click="editorStore.validationModal = false"><i class="icon-x"></i></button> - <button class="btn-choice accept" @click="confirmDelete">OUI, SUPPRIMER</button> - <button class="btn-choice cancel" @click="editorStore.validationModal = false"> - NON, NE PAS SUPPRIMER - </button> - </div> - </div> + <h3>Souhaitez-vous vraiment supprimer cet élément ?</h3> + </ChoiceModal> </template> <style lang="scss" scoped> -.modal-backdrop { - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - background-color: rgba(0, 0, 0, 0.3); - display: flex; - justify-content: center; - align-items: center; - z-index: 500; -} - -.modal { - user-select: none; - position: relative; - padding: 2rem; - background-color: white; - display: flex; - flex-direction: column; - border-radius: 10px; - gap: 1rem; - width: 22rem; -} - h3 { text-align: center; } - -.btn-choice { - cursor: pointer; - border: none; - border-radius: 30px; - font-size: 1rem; - font-weight: 500; - padding: 1rem; - - &:hover { - filter: brightness(95%); - } - &.cancel { - background-color: #fff; - color: var(--inria-grey); - border: 1px solid var(--inria-grey); - } - &.accept { - background-color: #e93100; - color: #fff; - } -} </style> diff --git a/src/shared/services/editor.service.ts b/src/shared/services/editor.service.ts index d66dada7731a291229d2c4b09e772cf2df922a7a..bd063213e0f66c65ab23317d45e9e6fba707085d 100644 --- a/src/shared/services/editor.service.ts +++ b/src/shared/services/editor.service.ts @@ -4,7 +4,7 @@ import { useEditorStore, useGraphStore, useUndoRedoStore } from '@/src/shared/st import { ePocProject } from '@/src/shared/interfaces'; import { createToaster } from '@meforma/vue-toaster'; import { closeFormPanel, graphService } from '.'; -import { createGraphEpocFromData } from '@/src/shared/services/import.service'; +import { createGraphEpocFromData, createGraphFromImport } from '@/src/shared/services/import.service'; import { useVueFlow } from '@vue-flow/core'; import { saveState } from '@/src/shared/services/undoRedo.service'; import { applyBackwardCompatibility } from '@/src/shared/utils/backwardCompability'; @@ -112,15 +112,11 @@ const setup = function () { }); api.receive('epocImportExtracted', (data: string) => { - const importedEpoc = JSON.parse(data); - graphStore.setFlow(null); - router.push('/editor').then(() => { - editorStore.loading = false; - if (!importedEpoc || !importedEpoc.workdir) return; - - createGraphEpocFromData(importedEpoc.epoc); - saveState(); - }); + createGraphFromImport(JSON.parse(data)); + }); + + api.receive('importRequired', (data: string) => { + editorStore.projectToImport = data; }); api.receive('previewReady', () => { @@ -186,6 +182,7 @@ function pickEpocProject(): void { function openEpocProject(project: ePocProject): void { editorStore.currentProject = project; editorStore.loading = true; + router.push('/landingpage').then(() => { api.send('openEpocProject', project.filepath); }); diff --git a/src/shared/services/import.service.ts b/src/shared/services/import.service.ts index cc807d053576f322d0886e47c609da79550e20b2..5f54449529f8202be540c1154b77966cd2b2e296 100644 --- a/src/shared/services/import.service.ts +++ b/src/shared/services/import.service.ts @@ -12,6 +12,9 @@ import { Assessment, ChoiceCondition, SimpleQuestion } from '@epoc/epoc-types/sr import { createRule, getConditions } from '@/src/shared/services/graph/badge.service'; import { Node } from '@vue-flow/core'; import { Badge } from '@/src/shared/interfaces'; +import { router } from '@/src/router'; +import { saveState } from '@/src/shared/services/undoRedo.service'; +import { useEditorStore, useGraphStore } from '@/src/shared/stores'; const mapType = { video: standardPages.find((s) => s.type === 'video'), @@ -196,3 +199,17 @@ function setQuestionData(type: string, question: any) { return questionData; } + +export function createGraphFromImport(importedEpoc) { + const graphStore = useGraphStore(); + const editorStore = useEditorStore(); + + graphStore.setFlow(null); + router.push('/editor').then(() => { + editorStore.loading = false; + if (!importedEpoc || !importedEpoc.workdir) return; + + createGraphEpocFromData(importedEpoc.epoc); + saveState(); + }); +} \ No newline at end of file diff --git a/src/shared/stores/editorStore.ts b/src/shared/stores/editorStore.ts index 2c67a1a7dd73b172fb15440f08d244c7149373b4..52b68714a02f7228a175a3c8bd2c793caa5891fb 100644 --- a/src/shared/stores/editorStore.ts +++ b/src/shared/stores/editorStore.ts @@ -10,6 +10,7 @@ type uid = string; interface EditorState { // Landing page loading: boolean; + projectToImport: string | null; recentProjects: ePocProject[]; currentProject: ePocProject; @@ -65,6 +66,7 @@ export const useEditorStore = defineStore('editor', { state: (): EditorState => ({ // Landing page loading: false, + projectToImport: null, recentProjects: [], currentProject: { filepath: null, workdir: null, name: null, modified: null }, diff --git a/src/views/LandingPage.vue b/src/views/LandingPage.vue index 4c0f68e8155b79c0ac7302b86ed2605e51a9cd7c..edd993040c45b821b0caacb6484a6f02edf2dfc8 100644 --- a/src/views/LandingPage.vue +++ b/src/views/LandingPage.vue @@ -2,6 +2,8 @@ import { useEditorStore } from '@/src/shared/stores'; import { editorService } from '@/src/shared/services'; import { ePocProject } from '@/src/shared/interfaces'; +import ChoiceModal from '@/src/components/ChoiceModal.vue'; +import { createGraphFromImport } from '@/src/shared/services/import.service'; const editorStore = useEditorStore(); @@ -17,6 +19,16 @@ function openProject(filepath: ePocProject) { function createProject() { editorService.newEpocProject(); } + +function cancelImport() { + editorStore.projectToImport = null; + editorStore.loading = false; +} + +function importProject() { + createGraphFromImport(JSON.parse(editorStore.projectToImport)); + editorStore.projectToImport = null; +} </script> <template> @@ -25,7 +37,25 @@ function createProject() { <img alt="logo epoc" src="/img/epoc.svg" /> <img alt="logo inria" src="/img/inria.svg" /> </div> - <div v-if="!editorStore.loading"> + + <ChoiceModal + v-if="editorStore.projectToImport" + accept-label="Importer" + @accept="importProject" + @cancel="cancelImport" + > + <h3>Cet ePoc est une publication, vous devez l'importer avant de pouvoir l'éditer ici</h3> + </ChoiceModal> + + <div v-if="editorStore.loading" class="loading"> + <div class="spinner"></div> + <span v-if="editorStore.currentProject.filepath"> + Chargement de "{{ editorStore.currentProject.filepath }}" + </span> + <span v-else>Chargement de l'ePoc</span> + </div> + + <div v-else> <div class="buttons"> <button class="btn btn-outline btn-large" @click="pickProject"> <i class="icon-ouvrir" /> @@ -52,13 +82,6 @@ function createProject() { </div> </div> </div> - <div v-if="editorStore.loading" class="loading"> - <div class="spinner"></div> - <span v-if="editorStore.currentProject.filepath"> - Chargement de "{{ editorStore.currentProject.filepath }}" - </span> - <span v-else>Chargement de l'ePoc</span> - </div> </div> </template>