diff --git a/package-lock.json b/package-lock.json index 17c2de0..77595d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,24 @@ { "name": "vcs-game-maker", - "version": "0.1.0", + "version": "0.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "0.1.0", + "version": "0.4.0", "dependencies": { "@vue/composition-api": "^1.0.0-rc.13", "batari-basic": "^0.0.1", "blockly": "^6.20210701.0", "core-js": "^3.6.5", + "file-saver": "^2.0.5", "handlebars": "^4.7.7", "lodash": "^4.17.21", "vue": "^2.6.11", "vue-code-highlight": "^0.7.8", "vue-router": "^3.2.0", - "vuetify": "^2.4.0" + "vuetify": "^2.4.0", + "yaml": "^1.10.2" }, "devDependencies": { "@vue/cli-plugin-babel": "~4.5.0", @@ -6463,6 +6465,11 @@ "node": ">= 8.9.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "node_modules/filesize": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", @@ -14847,6 +14854,14 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -20592,6 +20607,11 @@ "schema-utils": "^2.5.0" } }, + "file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "filesize": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", @@ -27585,6 +27605,11 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/package.json b/package.json index cb61e71..22a62c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vcs-game-maker", - "version": "0.4.0", + "version": "0.5.0", "private": true, "scripts": { "serve": "vue-cli-service serve", @@ -12,12 +12,14 @@ "batari-basic": "^0.0.1", "blockly": "^6.20210701.0", "core-js": "^3.6.5", + "file-saver": "^2.0.5", "handlebars": "^4.7.7", "lodash": "^4.17.21", "vue": "^2.6.11", "vue-code-highlight": "^0.7.8", "vue-router": "^3.2.0", - "vuetify": "^2.4.0" + "vuetify": "^2.4.0", + "yaml": "^1.10.2" }, "devDependencies": { "@vue/cli-plugin-babel": "~4.5.0", diff --git a/src/App.vue b/src/App.vue index 0391d3c..b2efe31 100644 --- a/src/App.vue +++ b/src/App.vue @@ -121,6 +121,19 @@ <v-list-item-title>Generated</v-list-item-title> </v-list-item-content> </v-list-item> + + <v-list-item + to="/project" + link + class="project-item" + > + <v-list-item-icon> + <v-icon>mdi-pencil-ruler</v-icon> + </v-list-item-icon> + <v-list-item-content> + <v-list-item-title>Project</v-list-item-title> + </v-list-item-content> + </v-list-item> </v-list> </v-navigation-drawer> @@ -216,4 +229,11 @@ export default { color: rgb(39, 176, 136) !important; border-left-color: rgb(39, 176, 136) !important; } + +.project-item, +.project-item > .v-list-item__icon > .theme--light.v-icon, +.project-item > .v-list-item__content { + color: rgb(39, 136, 176) !important; + border-left-color: rgb(39, 136, 176) !important; +} </style> diff --git a/src/components/ActionEditor.vue b/src/components/ActionEditor.vue index ca6a338..04710a0 100644 --- a/src/components/ActionEditor.vue +++ b/src/components/ActionEditor.vue @@ -21,7 +21,7 @@ import '../blocks/input'; import '../blocks/sprites'; import blocklyToolbox from 'raw-loader!./blockly-toolbox.xml'; import BlocklyBB from '../generators/bbasic'; -import {useLocalStorage} from '../hooks/storage'; +import {useWorkspaceStorage} from '../hooks/project'; import {useGeneratedBasic} from '../hooks/generated'; export default { @@ -40,7 +40,7 @@ export default { }, toolbox: blocklyToolbox, }, - workspaceStorage: useLocalStorage('vcs-game-maker.workspace'), + workspaceStorage: useWorkspaceStorage(), }), methods: { showCode() { diff --git a/src/components/BlocklyComponent.vue b/src/components/BlocklyComponent.vue index 299d8d1..d99e8a2 100644 --- a/src/components/BlocklyComponent.vue +++ b/src/components/BlocklyComponent.vue @@ -63,7 +63,7 @@ export default { }, handleChange() { const xml = Blockly.Xml.workspaceToDom(this.workspace); - const text = Blockly.Xml.domToText(xml); + const text = Blockly.Xml.domToPrettyText(xml); this.lastSavedWorkspace = text; this.$emit('input', text, { workspace: this.workspace, diff --git a/src/hooks/project.js b/src/hooks/project.js new file mode 100644 index 0000000..17ca53e --- /dev/null +++ b/src/hooks/project.js @@ -0,0 +1,4 @@ +import {useLocalStorage} from '../hooks/storage'; + +export const useProjectStorage = (type) => useLocalStorage(`vcs-game-maker.${type}`); +export const useWorkspaceStorage = () => useProjectStorage('workspace'); diff --git a/src/router/index.js b/src/router/index.js index 2ed1c2b..1b9ea4c 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -24,6 +24,11 @@ const routes = [ name: 'Generated', component: () => import('../views/GeneratedCode.vue'), }, + { + path: '/project', + name: 'Project', + component: () => import('../views/Project.vue'), + }, ]; const router = new VueRouter({ diff --git a/src/views/Project.vue b/src/views/Project.vue new file mode 100644 index 0000000..e129bfc --- /dev/null +++ b/src/views/Project.vue @@ -0,0 +1,84 @@ +<template> + <v-card> + <v-card-title>Project</v-card-title> + <v-card-text> + <v-file-input + accept=".vcsgm" + label="Project to import." + v-model="data.fileToImport" + @change="handleLoadProject" + ></v-file-input> + </v-card-text> + <v-card-actions> + <v-btn + color="primary" + @click="handleSaveProject" + > + Save Project + </v-btn> + </v-card-actions> + </v-card> +</template> +<script> +import {defineComponent, reactive} from '@vue/composition-api'; +import {saveAs} from 'file-saver'; +import YAML from 'yaml'; + +import {useWorkspaceStorage} from '../hooks/project'; + +const FORMAT_TYPE = 'VCS Game Maker Project'; +const FORMAT_VERSION = 1.0; + +export default defineComponent({ + setup(props, context) { + const data = reactive({fileToImport: null}); + const router = context.root.$router; + const workspaceStorage = useWorkspaceStorage(); + return {data, router, workspaceStorage}; + }, + methods: { + handleSaveProject() { + const projectYaml = YAML.stringify({ + 'type': FORMAT_TYPE, + 'format-version': FORMAT_VERSION, + 'generation-time': new Date(), + 'blockly-workspace': this.workspaceStorage, + }); + + const projectBlob = new Blob([projectYaml], {type: 'text/yaml'}); + saveAs(projectBlob, 'project.vcsgm'); + }, + + handleLoadProject() { + if (!this.data.fileToImport) { + console.warn('No file to import.'); + return; + } + + console.info('Importing file', this.data.fileToImport); + const reader = new FileReader(); + reader.readAsText(this.data.fileToImport, 'UTF-8'); + reader.onload = (evt) => { + const projectYaml = evt.target.result; + console.info('YAML', projectYaml); + const project = YAML.parse(projectYaml); + + if (project.type !== FORMAT_TYPE) { + throw new Error('This file does not seem to be a valid project.'); + } + + if (project['format-version'] > FORMAT_VERSION) { + throw new Error( + `This project's version (${project['format-version']}) is newer than the supported version (${FORMAT_VERSION})`); + } + + this.workspaceStorage = project['blockly-workspace']; + + this.router.push('/'); + }; + reader.onerror = (evt) => console.error('Error while loading project', evt); + this.data.fileToImport = null; + }, + }, +}); +</script>