aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--components/export/ExportModal.vue10
-rw-r--r--components/export/ExportZipButton.vue18
-rw-r--r--components/export/ExportZipModal.vue67
-rw-r--r--data/testData.json2
-rw-r--r--lib/questsLoader.ts22
-rw-r--r--package-lock.json68
-rw-r--r--package.json2
-rw-r--r--pages/import.vue4
-rw-r--r--plugins/jszip.ts5
-rw-r--r--stores/export.ts31
-rw-r--r--stores/session.ts12
-rw-r--r--utils/zipExporter.ts33
12 files changed, 264 insertions, 10 deletions
diff --git a/components/export/ExportModal.vue b/components/export/ExportModal.vue
index 8bc6936..1d09914 100644
--- a/components/export/ExportModal.vue
+++ b/components/export/ExportModal.vue
@@ -41,6 +41,10 @@ defineExpose({
<font-awesome-icon :icon="['fas', 'xmark']" />
You did not start this session by importing from file system.
</p>
+ <p class="error" v-if="canUseFsApi && isUsingFsMode">
+ <font-awesome-icon :icon="['fas', 'xmark']" />
+ Not yet implemented.
+ </p>
</div>
<div id="button-group">
@@ -56,6 +60,10 @@ defineExpose({
<p id="subtitle">Send to Server</p>
<p>Upload your quest configuration to the server, which can be downloaded and automatically applied in-game.
</p>
+ <p class="error">
+ <font-awesome-icon :icon="['fas', 'xmark']" />
+ Not yet implemented.
+ </p>
</div>
<div id="button-group">
@@ -73,7 +81,7 @@ defineExpose({
</div>
<div id="button-group">
- <Button type="solid" label="Continue" :disabled="true" />
+ <ExportZipButton />
</div>
</div>
</div>
diff --git a/components/export/ExportZipButton.vue b/components/export/ExportZipButton.vue
new file mode 100644
index 0000000..8fb6f60
--- /dev/null
+++ b/components/export/ExportZipButton.vue
@@ -0,0 +1,18 @@
+<script setup lang="ts">
+import type ExportZipModal from './ExportZipModal.vue';
+
+const exportZipModal = ref<InstanceType<typeof ExportZipModal> | null>(null);
+
+const startZipExport = async () => {
+ exportZipModal.value?.open();
+ exportZipModal.value?.startExport();
+}
+</script>
+
+<template>
+ <ClientOnly>
+ <Button type="solid" label="Continue" @click="startZipExport" />
+
+ <ExportZipModal ref="exportZipModal" />
+ </ClientOnly>
+</template> \ No newline at end of file
diff --git a/components/export/ExportZipModal.vue b/components/export/ExportZipModal.vue
new file mode 100644
index 0000000..b84690d
--- /dev/null
+++ b/components/export/ExportZipModal.vue
@@ -0,0 +1,67 @@
+<script setup lang="ts">
+import { saveAs } from 'file-saver';
+
+const session = useSessionStore();
+const exportStore = useExportStore();
+
+const showModal = ref(false);
+
+const status = computed(() => exportStore.getZipStatus());
+
+const startExport = async () => {
+ const quests = session.getQuests();
+ const categories = session.getCategories();
+ const items = session.getItems();
+
+ exportStore.setZipStatus('preparing');
+
+ const { transformedQuests, transformedCategories, transformedItems } = await prepareZip(quests, categories, items);
+
+ exportStore.setZipStatus('compressing');
+
+ try {
+ const blob = await createZip(transformedQuests, transformedCategories, transformedItems);
+
+ exportStore.setZipContents(blob);
+ exportStore.setZipStatus('ready');
+
+ saveAs(blob, "quests.zip");
+ } catch {
+ exportStore.setZipStatus('failed');
+ }
+}
+
+const open = () => {
+ showModal.value = true;
+}
+
+defineExpose({
+ open,
+ startExport
+})
+</script>
+
+<template>
+ <Modal v-model="showModal">
+ <template v-slot:header>
+ <h2>Export as ZIP</h2>
+ </template>
+
+ <p v-if="status === 'preparing'">Preparing quests for export...</p>
+ <p v-if="status === 'compressing'">Compressing zip...</p>
+ <p v-if="status === 'ready'">Done.</p>
+ <p v-if="status === 'failed'">Failed to create zip.</p>
+
+ <div id="controls" class="control-group">
+ <Button :icon="['fas', 'xmark']" :label="'Close'" @click="showModal = false"></Button>
+ </div>
+ </Modal>
+</template>
+
+<style scoped>
+#controls {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 1rem;
+}
+</style> \ No newline at end of file
diff --git a/data/testData.json b/data/testData.json
index 268a780..23f4604 100644
--- a/data/testData.json
+++ b/data/testData.json
@@ -384,7 +384,7 @@
"items": {
"coolsword": {
"type": "defined",
- "configuration": {
+ "item": {
"type": "DIAMOND_SWORD",
"name": "Super cool sword"
}
diff --git a/lib/questsLoader.ts b/lib/questsLoader.ts
index 296afc3..f49dce5 100644
--- a/lib/questsLoader.ts
+++ b/lib/questsLoader.ts
@@ -1,4 +1,4 @@
-import type { EditorQuest, EditorTask, EditorCategory } from '../stores/session';
+import type { EditorQuest, EditorTask, EditorCategory, EditorItem } from '../stores/session';
export function loadQuestsFromJson(config: any): EditorQuest[] {
return Object.keys(config).map((questid: any) => {
@@ -74,7 +74,7 @@ export function loadItemsFromJson(config: any): EditorItem[] {
return {
id: itemid,
type: item.type,
- config: item.config,
+ config: item.item,
};
});
}
@@ -119,4 +119,22 @@ export function mapJsonQuestToYamlObject(quest: EditorQuest): any {
...(quest.options.lockedDisplay && { lockedDisplay: quest.options.lockedDisplay }),
},
}
+}
+
+export function mapJsonCategoryToYamlObject(category: EditorCategory): any {
+ return {
+ display: {
+ name: category.display.name,
+ type: category.display.type,
+ lore: category.display.lore,
+ },
+ "permission-required": category.permissionRequired,
+ }
+}
+
+export function mapJsonItemToYamlObject(item: EditorItem): any {
+ return {
+ type: item.type,
+ item: item.config
+ }
} \ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 11c78fb..9d2ce49 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,8 @@
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.6",
"@pinia/nuxt": "^0.5.1",
+ "file-saver": "^2.0.5",
+ "jszip": "^3.10.1",
"nuxt": "^3.10.3",
"pinia": "^2.1.7",
"vue": "^3.4.15",
@@ -5844,6 +5846,11 @@
"node": "^10.12.0 || >=12.0.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/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -6440,6 +6447,11 @@
"resolved": "https://registry.npmjs.org/image-meta/-/image-meta-0.2.0.tgz",
"integrity": "sha512-ZBGjl0ZMEMeOC3Ns0wUF/5UdUmr3qQhBSCniT0LxOgGGIRHiNFOkMtIHB7EOznRU47V2AxPgiVP+s+0/UCU0Hg=="
},
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
+ },
"node_modules/immutable": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz",
@@ -6881,6 +6893,44 @@
"node >= 0.2.0"
]
},
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/jszip/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/jszip/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "node_modules/jszip/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -6976,6 +7026,14 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
"node_modules/lilconfig": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
@@ -9187,6 +9245,11 @@
"node": "^16.14.0 || >=18.0.0"
}
},
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -10448,6 +10511,11 @@
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
+ },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
diff --git a/package.json b/package.json
index bd11781..ef5b178 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,8 @@
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.6",
"@pinia/nuxt": "^0.5.1",
+ "file-saver": "^2.0.5",
+ "jszip": "^3.10.1",
"nuxt": "^3.10.3",
"pinia": "^2.1.7",
"vue": "^3.4.15",
diff --git a/pages/import.vue b/pages/import.vue
index f977538..7397358 100644
--- a/pages/import.vue
+++ b/pages/import.vue
@@ -37,6 +37,10 @@ const discardSessionModal = ref<InstanceType<typeof LoaderDiscardSessionModal> |
<div id="description">
<p id="subtitle">Retrieve from In-Game</p>
<p>Download quest data uploaded from in-game.</p>
+ <p class="error">
+ <font-awesome-icon :icon="['fas', 'xmark']" />
+ Not yet implemented.
+ </p>
</div>
<div id="button-group">
diff --git a/plugins/jszip.ts b/plugins/jszip.ts
new file mode 100644
index 0000000..edf0206
--- /dev/null
+++ b/plugins/jszip.ts
@@ -0,0 +1,5 @@
+import JSZip from "jszip"
+
+export default defineNuxtPlugin((nuxtApp) => {
+ nuxtApp.vueApp.use(JSZip)
+})
diff --git a/stores/export.ts b/stores/export.ts
new file mode 100644
index 0000000..3f48aa3
--- /dev/null
+++ b/stores/export.ts
@@ -0,0 +1,31 @@
+import { defineStore } from 'pinia'
+
+export type ZipLoaderStatus = 'inactive' | 'preparing' | 'compressing' | 'ready' | 'failed';
+
+export const useExportStore = defineStore('export', {
+ state: () => ({
+ zip: {
+ status: 'inactive' as ZipLoaderStatus,
+ contents: null as Blob | null,
+ }
+ }),
+ getters: {
+ getZipStatus: (state) => () => {
+ return state.zip.status;
+ },
+ getZipContents: (state) => () => {
+ return state.zip.contents;
+ },
+ },
+ actions: {
+ setZipStatus(status: ZipLoaderStatus) {
+ this.zip.status = status;
+ if (status === 'inactive' || status === 'preparing') {
+ this.zip.contents = null;
+ }
+ },
+ setZipContents(contents: Blob) {
+ this.zip.contents = contents;
+ },
+ }
+});
diff --git a/stores/session.ts b/stores/session.ts
index 876b6b0..50bcde7 100644
--- a/stores/session.ts
+++ b/stores/session.ts
@@ -111,14 +111,14 @@ export const useSessionStore = defineStore('session', {
getSessionType: (state) => () => {
return state.sessionType
},
- getQuests(): EditorQuest[] {
- return this.session.quests
+ getQuests: (state) => () => {
+ return state.session.quests
},
- getCategories(): EditorCategory[] {
- return this.session.categories
+ getCategories: (state) => () => {
+ return state.session.categories
},
- getItems(): EditorItem[] {
- return this.session.items
+ getItems: (state) => () => {
+ return state.session.items
},
getQuestById: (state) => (id: string) => {
if (!id) return null;
diff --git a/utils/zipExporter.ts b/utils/zipExporter.ts
new file mode 100644
index 0000000..635e89d
--- /dev/null
+++ b/utils/zipExporter.ts
@@ -0,0 +1,33 @@
+import JSZip from "jszip";
+import { stringify } from "yaml";
+import { mapJsonCategoryToYamlObject, mapJsonItemToYamlObject, mapJsonQuestToYamlObject } from "~/lib/questsLoader";
+
+//TODO include the main configuration
+export async function prepareZip(quests: EditorQuest[], categories: EditorCategory[], items: EditorItem[]) {
+ const transformedQuests = Object.fromEntries(quests.map((quest) => [quest.id, stringify(mapJsonQuestToYamlObject(quest))]));
+ const transformedItems = Object.fromEntries(items.map((item) => [item.id, stringify(mapJsonItemToYamlObject(item))]));
+ const transformedCategories = stringify(Object.fromEntries(categories.map((category) => [category.id, mapJsonCategoryToYamlObject(category)])));
+
+ return {
+ transformedQuests,
+ transformedItems,
+ transformedCategories
+ }
+}
+
+export async function createZip(quests: { [key: string]: string }, categories: string, items: { [key: string]: string }) {
+ const zip = new JSZip();
+
+ zip.file("categories.yml", categories);
+
+ const questsDirectory = zip.folder("quests");
+ Object.entries(quests).forEach(([key, value]) => {
+ questsDirectory?.file(`${key}.yml`, value)
+ })
+ const itemsDirectory = zip.folder("items");
+ Object.entries(items).forEach(([key, value]) => {
+ itemsDirectory?.file(`${key}.yml`, value)
+ })
+
+ return await zip.generateAsync({ type: "blob" });
+} \ No newline at end of file