Skip to content

Commit

Permalink
Loading Screen!
Browse files Browse the repository at this point in the history
  • Loading branch information
ltouroumov committed Sep 20, 2024
1 parent fdb4b2b commit 4564561
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 80 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@typescript-eslint/no-unused-vars": "off",
"no-console": "off",
"vue/multi-word-component-names": "off",
"vue/no-multiple-template-root": "off",
"sort-imports": [
"error",
{
Expand Down
21 changes: 7 additions & 14 deletions components/LoadProject.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@
import { useProjectStore } from '~/composables/store/project';
import { readFileContents } from '~/composables/utils';
const { loadProject, unloadProject } = useProjectStore();
const { isLoaded } = useProjectStore();
const { loadProject } = useProjectStore();
const fileInput = ref<HTMLInputElement>();
const isLoading = ref<boolean>(false);
const canLoad = ref<boolean>(false);
Expand Down Expand Up @@ -65,21 +64,15 @@ const loadProjectFile = async () => {
return;
}
isLoading.value = true;
try {
await loadProject(async () => {
const data = await readFileContents(file);
if (data && typeof data === 'string') {
isLoading.value = false;
unloadProject();
await loadProject(data, file.name);
}
} catch (e) {
isLoading.value = false;
if (e instanceof Error) {
error.value = e.message;
return {
fileContents: data,
fileName: file.name,
};
}
}
});
}
};
</script>
Expand Down
78 changes: 37 additions & 41 deletions components/viewer/ProjectMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<script setup lang="ts">
import { useProjectRefs, useProjectStore } from '~/composables/store/project';
import { useViewerStore } from '~/composables/store/viewer';
import { bufferToString } from '~/composables/utils';
import { bufferToString, sleep } from '~/composables/utils';
import { ViewerProjectList } from '~/composables/viewer';
const isLoading = ref<boolean>(false);
Expand All @@ -53,54 +53,50 @@ const loadRemoteFile = async ({ target }: MouseEvent) => {
if (target && target instanceof HTMLAnchorElement) {
const fileURL = target.dataset.fileurl;
if (!fileURL) return;
isLoading.value = true;
const response = await fetch(fileURL);
let result: string;
if (response.ok) {
const reader = response.body!.getReader();
toggleProjectMenu(false);
await loadProject(async (setProgress) => {
const response = await fetch(fileURL);
if (response.ok) {
const reader = response.body!.getReader();
let received = 0;
const chunks = [];
let received = 0;
const chunks = [];
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value } = await reader.read();
if (done) break;
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
chunks.push(value);
received += value.length;
await nextTick(() => {
const receivedMB = received / (1024 * 1024);
const neat = Math.round(receivedMB * 100) / 100;
progress.value = `Downloaded ${neat} Mb`;
});
}
progress.value = `Loading ${target.text}...`;
// A hack, but otherwise the progress value never updates before loadProject is called
const pause = new Promise((resolve) => setTimeout(resolve, 100));
await pause;
const bodyBytes = new Uint8Array(received);
let pos = 0;
for (const chunk of chunks) {
bodyBytes.set(chunk, pos);
pos += chunk.length;
await setProgress(`Downloaded ${neat} Mb`);
}
await setProgress(`Loading ${target.text}...`);
// A hack, but otherwise the progress value never updates before loadProject is called
await sleep(100);
const bodyBytes = new Uint8Array(received);
let pos = 0;
for (const chunk of chunks) {
bodyBytes.set(chunk, pos);
pos += chunk.length;
}
return {
fileContents: bufferToString(bodyBytes),
fileName: fileURL.toString(),
};
} else {
throw new Error(
`HTTP Request failed with ${response.status}: ${response.statusText}`,
);
}
const bodyText = bufferToString(bodyBytes);
result = bodyText;
} else {
return;
}
unloadProject();
await loadProject(result, fileURL);
toggleProjectMenu(false);
isLoading.value = false;
});
}
};
</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import SearchModal from '~/components/viewer/modal/SearchModal.vue';
import StyleProject from '~/components/viewer/style/StyleProject.vue';
import ViewMenuBar from '~/components/viewer/ViewMenuBar.vue';
const { project } = defineProps<{
defineProps<{
project: Project;
}>();
</script>
Expand Down
34 changes: 34 additions & 0 deletions components/viewer/ProjectViewWrapper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<ProjectView v-if="isNotNil($store.project)" :project="$store.project.data" />
<div v-if="$store.store.status === 'loading'" class="loading-overlay">
<div class="spinner-border text-primary me-2" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<strong>
{{ !$store.store.progress ? 'Loading ...' : $store.store.progress }}
</strong>
</div>
</template>

<script setup lang="ts">
import { isNotNil } from 'ramda';
import ProjectView from '~/components/viewer/ProjectView.vue';
import { useProjectStore } from '~/composables/store/project';
const $store = useProjectStore();
</script>

<style lang="scss" scoped>
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
</style>
17 changes: 17 additions & 0 deletions composables/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,20 @@ export type ProjectFile = {
projectName: string;
projectHash: string;
};

export type EmptyProjectStore = {
status: 'empty';
};
export type LoadingProjectStore = {
status: 'loading';
progress?: string;
};
export type LoadedProjectStore = {
status: 'loaded';
file: ProjectFile;
};

export type ProjectStore =
| EmptyProjectStore
| LoadingProjectStore
| LoadedProjectStore;
86 changes: 67 additions & 19 deletions composables/store/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,36 @@ import {
ProjectFile,
ProjectObj,
ProjectRow,
ProjectStore,
Score,
} from '~/composables/project';
import { bufferToHex, stringToBuffer } from '~/composables/utils';

export type Selections = Record<string, number>;
type Transform = (sel: Selections) => Selections;

export type LoadProjectData = {
fileContents: string;
fileName: string;
};

type SetProgressF = (progress: string) => Promise<void>;
type ProjectProvider = (
setProgress: SetProgressF,
) => Promise<LoadProjectData | undefined>;

export const useProjectStore = defineStore('project', () => {
const $toast = useToast();

const project = shallowRef<ProjectFile | null>(null);
const store = shallowRef<ProjectStore>({
status: 'empty',
});

const project = computed<ProjectFile | null>(() => {
if (store.value.status === 'loaded') return store.value.file;
else return null;
});

const selected = ref<Selections>({});
const selectedIds = computed(() => R.keys(selected.value));

Expand Down Expand Up @@ -77,27 +96,55 @@ export const useProjectStore = defineStore('project', () => {
});

const isLoaded = computed(() => !!project.value);
const loadProject = async (fileContents: string, fileName: string) => {
const hashBytes = await crypto.subtle.digest(
'SHA-1',
stringToBuffer(fileContents),
);
const hashHex = bufferToHex(hashBytes);

const data: Project = JSON.parse(fileContents);
const projectFile: ProjectFile = {
data: data,
fileName: fileName,
projectId: data?.$projectId ?? hashHex,
projectName: data.rows[0].title,
projectHash: hashHex,
};
console.log(projectFile);
project.value = projectFile;

const loadProject = async (provider: ProjectProvider) => {
store.value = { status: 'loading' };
selected.value = {};

const setProgress = (progress: string): Promise<void> =>
nextTick(() => {
(store.value as LoadingProjectStore).progress = progress;
triggerRef(store);
});

try {
const result = await provider(setProgress);
if (!result) {
store.value = { status: 'empty' };
return;
}

const { fileContents, fileName } = result;

const hashBytes = await crypto.subtle.digest(
'SHA-1',
stringToBuffer(fileContents),
);
const hashHex = bufferToHex(hashBytes);

const data: Project = JSON.parse(fileContents);
const projectFile: ProjectFile = {
data: data,
fileName: fileName,
projectId: data?.$projectId ?? hashHex,
projectName: data.rows[0].title,
projectHash: hashHex,
};
console.log(projectFile);

store.value = {
status: 'loaded',
file: projectFile,
};
triggerRef(store);
} catch (e) {
$toast.error('Failed to load the project :(');
console.log(e);
store.value = { status: 'empty' };
}
};
const unloadProject = () => {
project.value = null;
store.value = { status: 'empty' };
selected.value = {};
};

Expand Down Expand Up @@ -376,6 +423,7 @@ export const useProjectStore = defineStore('project', () => {
});

return {
store,
project,
projectRows,
backpack,
Expand Down
3 changes: 3 additions & 0 deletions composables/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,6 @@ export const readFileContents = (file: Blob) => {
}
});
};

export const sleep = (timeout: number) =>
new Promise((resolve) => setTimeout(resolve, timeout));
8 changes: 3 additions & 5 deletions pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<ViewProject v-if="project" :project="project.data" />
<div v-else class="dialog-container">
<ProjectViewWrapper />
<div v-if="store.status === 'empty'" class="dialog-container">
<div class="bg-dark-subtle dialog text-light">
<ProjectMenu :project-list="projectList" />
</div>
Expand All @@ -9,12 +9,10 @@

<script setup lang="ts">
import { definePageMeta } from '#imports';
import ProjectMenu from '~/components/viewer/ProjectMenu.vue';
import ViewProject from '~/components/viewer/ViewProject.vue';
import { useProjectRefs } from '~/composables/store/project';
import { useViewerRefs } from '~/composables/store/viewer';
const { project } = useProjectRefs();
const { store } = useProjectRefs();
const { viewerProjectList } = useViewerRefs();
const projectList = computed(() => viewerProjectList.value);
Expand Down

0 comments on commit 4564561

Please sign in to comment.