mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-17 03:34:56 +02:00
Implemented loading state for potentially long running operations
This commit is contained in:
@@ -75,7 +75,6 @@ npm run lint
|
||||
|
||||
* Implement iframe based rendering (as more compatible alternative to OffscreenCanvas) for effects
|
||||
* Implement action queue when drawing, only execute drawing on update() hook
|
||||
* Implement loaders on document load/save, image export and dropbox import
|
||||
* Maintain cache for transformations and filters, rendered at the display destination size (invalidate on window resize)
|
||||
* Drawing masks on a rotated layer that is panned (or mirrored) is broken
|
||||
* Dragging of masks on rotated/mirror content is kinda broken
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2020 - https://www.igorski.nl
|
||||
* Igor Zinken 2020-2021 - https://www.igorski.nl
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
@@ -56,13 +56,14 @@
|
||||
@close="closeModal()"
|
||||
/>
|
||||
</div>
|
||||
<loader v-if="isLoading" />
|
||||
<!-- notifications -->
|
||||
<notifications />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vuex, { mapState, mapMutations, mapActions } from "vuex";
|
||||
import Vuex, { mapState, mapGetters, mapMutations, mapActions } from "vuex";
|
||||
import Vue from "vue";
|
||||
import VueI18n from "vue-i18n";
|
||||
import VTooltip from "v-tooltip";
|
||||
@@ -71,7 +72,8 @@ import DocumentCanvas from "@/components/document-canvas/document-canvas";
|
||||
import OptionsPanel from "@/components/options-panel/options-panel";
|
||||
import Toolbox from "@/components/toolbox/toolbox";
|
||||
import DialogWindow from "@/components/dialog-window/dialog-window";
|
||||
import Notifications from '@/components/notifications/notifications';
|
||||
import Notifications from "@/components/notifications/notifications";
|
||||
import Loader from "@/components/loader/loader";
|
||||
import { isMobile } from "@/utils/environment-util";
|
||||
import ToolTypes from "@/definitions/tool-types";
|
||||
import store from "./store";
|
||||
@@ -95,11 +97,12 @@ export default {
|
||||
store: new Vuex.Store( store ),
|
||||
components: {
|
||||
ApplicationMenu,
|
||||
DocumentCanvas,
|
||||
Toolbox,
|
||||
DialogWindow,
|
||||
OptionsPanel,
|
||||
DocumentCanvas,
|
||||
Loader,
|
||||
Notifications,
|
||||
OptionsPanel,
|
||||
Toolbox,
|
||||
},
|
||||
data: () => ({
|
||||
docWidth: "100%",
|
||||
@@ -113,6 +116,9 @@ export default {
|
||||
"modal",
|
||||
"windowSize",
|
||||
]),
|
||||
...mapGetters([
|
||||
"isLoading",
|
||||
]),
|
||||
activeModal() {
|
||||
switch ( this.modal ) {
|
||||
default:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2020 - https://www.igorski.nl
|
||||
* Igor Zinken 2020-2021 - https://www.igorski.nl
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
@@ -183,6 +183,8 @@ export default {
|
||||
"closeModal",
|
||||
"showNotification",
|
||||
"setDropboxConnected",
|
||||
"setLoading",
|
||||
"unsetLoading",
|
||||
]),
|
||||
...mapActions([
|
||||
"loadDocument",
|
||||
@@ -212,6 +214,7 @@ export default {
|
||||
this.loading = false;
|
||||
},
|
||||
async handleNodeClick( node ) {
|
||||
this.setLoading( "dbox" );
|
||||
switch ( node.type ) {
|
||||
case "folder":
|
||||
await this.retrieveFiles( node.path );
|
||||
@@ -233,6 +236,7 @@ export default {
|
||||
this.closeModal();
|
||||
break;
|
||||
}
|
||||
this.unsetLoading( "dbox" );
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2019-2020 - https://www.igorski.nl
|
||||
* Igor Zinken 2020-2021 - https://www.igorski.nl
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
@@ -110,10 +110,14 @@ export default {
|
||||
methods: {
|
||||
...mapMutations([
|
||||
"closeModal",
|
||||
"setLoading",
|
||||
"unsetLoading",
|
||||
]),
|
||||
async exportImage() {
|
||||
this.setLoading( "exp" );
|
||||
const blob = await createDocumentSnapshot( this.activeDocument, this.type, this.quality );
|
||||
saveBlobAsFile( blob, `${this.name}.${typeToExt(this.type)}` );
|
||||
this.unsetLoading( "exp" );
|
||||
this.closeModal();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2020 - https://www.igorski.nl
|
||||
* Igor Zinken 2020-2021 - https://www.igorski.nl
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
@@ -88,12 +88,15 @@ export default {
|
||||
"openDialog",
|
||||
"setActiveDocumentName",
|
||||
"showNotification",
|
||||
"setLoading",
|
||||
"unsetLoading",
|
||||
]),
|
||||
async requestSave() {
|
||||
if ( !this.isValid ) {
|
||||
return;
|
||||
}
|
||||
this.setActiveDocumentName( this.name );
|
||||
this.setLoading( "save" );
|
||||
try {
|
||||
const blob = DocumentFactory.toBlob( this.activeDocument );
|
||||
await uploadBlob( blob, `${this.name}${PROJECT_FILE_EXTENSION}` );
|
||||
@@ -101,6 +104,7 @@ export default {
|
||||
} catch ( e ) {
|
||||
this.openDialog({ type: "error", message: this.$t( "errorOccurred" ) });
|
||||
}
|
||||
this.unsetLoading( "save" );
|
||||
this.closeModal();
|
||||
},
|
||||
},
|
||||
|
||||
189
src/components/loader/loader.vue
Normal file
189
src/components/loader/loader.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2019 - https://www.igorski.nl
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
<template> <!-- functional> // https://github.com/vuejs/vue/issues/8822 -->
|
||||
<div class="loader">
|
||||
<div class="uil-default-css">
|
||||
<div class="element" style="-webkit-transform: rotate(0deg) translate(0,-60px);transform: rotate(0deg) translate(0,-60px);"></div>
|
||||
<div class="element" style="-webkit-transform:rotate(30deg) translate(0,-60px);transform:rotate(30deg) translate(0,-60px);"></div>
|
||||
<div class="element" style="-webkit-transform:rotate(60deg) translate(0,-60px);transform:rotate(60deg) translate(0,-60px);"></div>
|
||||
<div class="element" style="-webkit-transform:rotate(90deg) translate(0,-60px);transform:rotate(90deg) translate(0,-60px);"></div>
|
||||
<div class="element" style="-webkit-transform:rotate(120deg) translate(0,-60px);transform:rotate(120deg) translate(0,-60px);"></div>
|
||||
<div class="element" style="-webkit-transform:rotate(150deg) translate(0,-60px);transform:rotate(150deg) translate(0,-60px);"></div>
|
||||
<div class="element" style="-webkit-transform:rotate(180deg) translate(0,-60px);transform:rotate(180deg) translate(0,-60px);"></div>
|
||||
<div class="element" style="-webkit-transform:rotate(210deg) translate(0,-60px);transform:rotate(210deg) translate(0,-60px);"></div>
|
||||
<div class="element" style="-webkit-transform:rotate(240deg) translate(0,-60px);transform:rotate(240deg) translate(0,-60px);"></div>
|
||||
<div class="element" style="-webkit-transform:rotate(270deg) translate(0,-60px);transform:rotate(270deg) translate(0,-60px);"></div>
|
||||
<div class="element" style="-webkit-transform:rotate(300deg) translate(0,-60px);transform:rotate(300deg) translate(0,-60px);"></div>
|
||||
<div class="element" style="-webkit-transform:rotate(330deg) translate(0,-60px);transform:rotate(330deg) translate(0,-60px);"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/_mixins";
|
||||
|
||||
$loaderSize: 200px;
|
||||
|
||||
.loader {
|
||||
position: fixed;
|
||||
background-image: $color-window-bg;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: $loaderSize;
|
||||
height: $loaderSize;
|
||||
margin-left: -( $loaderSize / 2 );
|
||||
margin-top: -( $loaderSize / 2 );
|
||||
border-radius: 24px;
|
||||
border-bottom: #{$spacing-medium - $spacing-xsmall} solid #000;
|
||||
transform: scale( 0.33 );
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
.element {
|
||||
position:absolute;
|
||||
top: 80px;
|
||||
left: 93px;
|
||||
width: $spacing-medium;
|
||||
border-radius: $spacing-medium;
|
||||
height: 40px;
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
/* generated by loading.io */
|
||||
|
||||
@-webkit-keyframes uil-default-anim {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes uil-default-anim {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.uil-default-css > div:nth-of-type(1) {
|
||||
-webkit-animation: uil-default-anim 1s linear infinite;
|
||||
animation: uil-default-anim 1s linear infinite;
|
||||
-webkit-animation-delay: -0.5s;
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
.uil-default-css {
|
||||
position: relative;
|
||||
width: $loaderSize;
|
||||
height: $loaderSize;
|
||||
transform:scale(0.75);
|
||||
}
|
||||
|
||||
.uil-default-css > div:nth-of-type(2) {
|
||||
-webkit-animation: uil-default-anim 1s linear infinite;
|
||||
animation: uil-default-anim 1s linear infinite;
|
||||
-webkit-animation-delay: -0.4166666666666667s;
|
||||
animation-delay: -0.4166666666666667s;
|
||||
}
|
||||
|
||||
.uil-default-css > div:nth-of-type(3) {
|
||||
-webkit-animation: uil-default-anim 1s linear infinite;
|
||||
animation: uil-default-anim 1s linear infinite;
|
||||
-webkit-animation-delay: -0.33333333333333337s;
|
||||
animation-delay: -0.33333333333333337s;
|
||||
}
|
||||
|
||||
.uil-default-css > div:nth-of-type(4) {
|
||||
-webkit-animation: uil-default-anim 1s linear infinite;
|
||||
animation: uil-default-anim 1s linear infinite;
|
||||
-webkit-animation-delay: -0.25s;
|
||||
animation-delay: -0.25s;
|
||||
}
|
||||
|
||||
.uil-default-css > div:nth-of-type(5) {
|
||||
-webkit-animation: uil-default-anim 1s linear infinite;
|
||||
animation: uil-default-anim 1s linear infinite;
|
||||
-webkit-animation-delay: -0.16666666666666669s;
|
||||
animation-delay: -0.16666666666666669s;
|
||||
}
|
||||
|
||||
.uil-default-css > div:nth-of-type(6) {
|
||||
-webkit-animation: uil-default-anim 1s linear infinite;
|
||||
animation: uil-default-anim 1s linear infinite;
|
||||
-webkit-animation-delay: -0.08333333333333331s;
|
||||
animation-delay: -0.08333333333333331s;
|
||||
}
|
||||
|
||||
.uil-default-css > div:nth-of-type(7) {
|
||||
-webkit-animation: uil-default-anim 1s linear infinite;
|
||||
animation: uil-default-anim 1s linear infinite;
|
||||
-webkit-animation-delay: 0s;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.uil-default-css > div:nth-of-type(8) {
|
||||
-webkit-animation: uil-default-anim 1s linear infinite;
|
||||
animation: uil-default-anim 1s linear infinite;
|
||||
-webkit-animation-delay: 0.08333333333333337s;
|
||||
animation-delay: 0.08333333333333337s;
|
||||
}
|
||||
|
||||
.uil-default-css > div:nth-of-type(9) {
|
||||
-webkit-animation: uil-default-anim 1s linear infinite;
|
||||
animation: uil-default-anim 1s linear infinite;
|
||||
-webkit-animation-delay: 0.16666666666666663s;
|
||||
animation-delay: 0.16666666666666663s;
|
||||
}
|
||||
|
||||
.uil-default-css > div:nth-of-type(10) {
|
||||
-webkit-animation: uil-default-anim 1s linear infinite;
|
||||
animation: uil-default-anim 1s linear infinite;
|
||||
-webkit-animation-delay: 0.25s;
|
||||
animation-delay: 0.25s;
|
||||
}
|
||||
|
||||
.uil-default-css > div:nth-of-type(11) {
|
||||
-webkit-animation: uil-default-anim 1s linear infinite;
|
||||
animation: uil-default-anim 1s linear infinite;
|
||||
-webkit-animation-delay: 0.33333333333333337s;
|
||||
animation-delay: 0.33333333333333337s;
|
||||
}
|
||||
|
||||
.uil-default-css > div:nth-of-type(12) {
|
||||
-webkit-animation: uil-default-anim 1s linear infinite;
|
||||
animation: uil-default-anim 1s linear infinite;
|
||||
-webkit-animation-delay: 0.41666666666666663s;
|
||||
animation-delay: 0.41666666666666663s;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2020 - https://www.igorski.nl
|
||||
* Igor Zinken 2020-2021 - https://www.igorski.nl
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
@@ -56,6 +56,7 @@ export default {
|
||||
panMode: false, // whether drag interactions with the document will pan its viewport
|
||||
dialog: null, // currently opened dialog
|
||||
modal: null, // currently opened modal
|
||||
loadingStates: [], // wether one or more long running operations are running
|
||||
notifications: [], // notification message queue
|
||||
dropboxConnected: false,
|
||||
windowSize: {
|
||||
@@ -66,6 +67,7 @@ export default {
|
||||
getters: {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
t: state => ( key, optArgs ) => translate( key, optArgs ),
|
||||
isLoading: state => state.loadingStates.length > 0,
|
||||
},
|
||||
mutations: {
|
||||
setMenuOpened( state, value ) {
|
||||
@@ -86,6 +88,17 @@ export default {
|
||||
setPanMode( state, value ) {
|
||||
state.panMode = value;
|
||||
},
|
||||
setLoading( state, key ) {
|
||||
if ( !state.loadingStates.includes( key )) {
|
||||
state.loadingStates.push( key );
|
||||
}
|
||||
},
|
||||
unsetLoading( state, key ) {
|
||||
const idx = state.loadingStates.indexOf( key );
|
||||
if ( idx > -1 ) {
|
||||
state.loadingStates.splice( idx, 1 );
|
||||
}
|
||||
},
|
||||
/**
|
||||
* open a dialog window showing given title and message.
|
||||
* types can be info, error or confirm. When type is confirm, optional
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import store, { PROJECT_FILE_EXTENSION } from "@/store";
|
||||
import { LAYER_IMAGE } from "@/definitions/layer-types";
|
||||
|
||||
const { mutations, actions } = store;
|
||||
const { getters, mutations, actions } = store;
|
||||
|
||||
let mockUpdateFn;
|
||||
jest.mock( "@/services/keyboard-service", () => ({
|
||||
@@ -19,6 +19,15 @@ jest.mock( "@/utils/file-util", () => ({
|
||||
}))
|
||||
|
||||
describe( "Vuex store", () => {
|
||||
describe( "getters", () => {
|
||||
it( "should know when there is currently a loading state active", () => {
|
||||
const state = { loadingStates: [] };
|
||||
expect( getters.isLoading( state )).toBe( false );
|
||||
state.loadingStates.push( "foo" );
|
||||
expect( getters.isLoading( state )).toBe( true );
|
||||
});
|
||||
});
|
||||
|
||||
describe( "mutations", () => {
|
||||
it( "should be able to toggle the opened state of the menu", () => {
|
||||
const state = { menuOpened: false };
|
||||
@@ -57,6 +66,20 @@ describe( "Vuex store", () => {
|
||||
expect( state.panMode ).toBe( true );
|
||||
});
|
||||
|
||||
describe( "when toggling loading states", () => {
|
||||
it( "should be able to register a new loading state", () => {
|
||||
const state = { loadingStates: [ "foo" ] };
|
||||
mutations.setLoading( state, "bar" );
|
||||
expect( state.loadingStates ).toEqual([ "foo", "bar" ]);
|
||||
});
|
||||
|
||||
it( "should be able to unregister an existing loading state", () => {
|
||||
const state = { loadingStates: [ "foo", "bar" ] };
|
||||
mutations.unsetLoading( state, "foo" );
|
||||
expect( state.loadingStates ).toEqual([ "bar" ]);
|
||||
});
|
||||
});
|
||||
|
||||
describe( "when toggling dialog windows", () => {
|
||||
it( "should be able to open a dialog window and apply its request parameters", () => {
|
||||
const state = { dialog: null };
|
||||
|
||||
Reference in New Issue
Block a user