Upload: Refactor UX

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-01-30 01:53:18 +01:00
parent b3a50695c0
commit 777526ce82
11 changed files with 252 additions and 28 deletions

View file

@ -7,6 +7,12 @@
<v-toolbar-title class="p-navigation-title">{{ page.title }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items>
<v-btn icon @click.stop="showUpload = true" v-if="!readonly">
<v-icon>cloud_upload</v-icon>
</v-btn>
</v-toolbar-items>
</v-toolbar>
<v-navigation-drawer
v-model="drawer"
@ -172,7 +178,7 @@
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/discover" @click="" class="p-navigation-discover" v-if="config.experimental">
<!-- v-list-tile to="/discover" @click="" class="p-navigation-discover" v-if="config.experimental">
<v-list-tile-action>
<v-icon>color_lens</v-icon>
</v-list-tile-action>
@ -182,7 +188,7 @@
<translate>Discover</translate>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list-tile -->
<!-- v-list-tile to="/events" @click="" class="p-navigation-events">
<v-list-tile-action>
@ -228,7 +234,7 @@
</v-list-tile-content>
</v-list-tile>
<v-list-tile @click="logout" class="p-navigation-logout" v-if="!isPublic && auth">
<v-list-tile @click="logout" class="p-navigation-logout" v-if="!public && auth">
<v-list-tile-action>
<v-icon>power_settings_new</v-icon>
</v-list-tile-action>
@ -251,14 +257,18 @@
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<p-upload-dialog :show="showUpload" @cancel="showUpload = false"
@confirm="showUpload = false"></p-upload-dialog>
</div>
</template>
<script>
import Album from "../model/album";
import {DateTime} from "luxon";
import Event from "pubsub-js";
export default {
name: "p-navigation",
@ -269,14 +279,17 @@
drawer: null,
mini: mini,
session: this.$session,
isPublic: this.$config.getValue("public"),
public: this.$config.getValue("public"),
readonly: this.$config.getValue("readonly"),
config: this.$config.values,
page: this.$config.page,
showUpload: false,
uploadSubId: null,
};
},
computed: {
auth() {
return this.session.auth || this.isPublic
return this.session.auth || this.public
},
},
methods: {
@ -292,6 +305,12 @@
logout() {
this.$session.logout();
},
},
created() {
this.uploadSubId = Event.subscribe("upload.show", () => this.showUpload = true);
},
destroyed() {
Event.unsubscribe(this.uploadSubId);
}
};
</script>

View file

@ -17,7 +17,7 @@
<v-spacer></v-spacer>
<v-btn icon @click.stop="refresh" class="hidden-xs-only">
<v-btn icon @click.stop="refresh" class="hidden-xs-only" :class="dirty ? 'secondary-light': ''">
<v-icon>refresh</v-icon>
</v-btn>
@ -31,6 +31,10 @@
<v-icon>view_column</v-icon>
</v-btn>
<v-btn icon @click.stop="showUpload()" v-if="!this.$config.values.readonly" class="hidden-md-and-down">
<v-icon>cloud_upload</v-icon>
</v-btn>
<v-btn icon @click.stop="searchExpanded = !searchExpanded" class="p-expand-search">
<v-icon>{{ searchExpanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }}</v-icon>
</v-btn>
@ -133,9 +137,12 @@
</v-form>
</template>
<script>
import Event from "pubsub-js";
export default {
name: 'p-photo-search',
props: {
dirty: Boolean,
filter: Object,
settings: Object,
refresh: Function,
@ -223,6 +230,9 @@
this.filter.q = '';
this.filterChange();
},
showUpload() {
Event.publish("upload.show");
}
},
};
</script>

View file

@ -3,6 +3,7 @@ import PPhotoAlbumDialog from "./p-photo-album-dialog.vue";
import PPhotoEditDialog from "./p-photo-edit-dialog.vue";
import PPhotoShareDialog from "./p-photo-share-dialog.vue";
import PAlbumDeleteDialog from "./p-album-delete-dialog.vue";
import PUploadDialog from "./p-upload-dialog.vue";
const dialogs = {};
@ -12,6 +13,7 @@ dialogs.install = (Vue) => {
Vue.component("p-photo-edit-dialog", PPhotoEditDialog);
Vue.component("p-photo-share-dialog", PPhotoShareDialog);
Vue.component("p-album-delete-dialog", PAlbumDeleteDialog);
Vue.component("p-upload-dialog", PUploadDialog);
};
export default dialogs;

View file

@ -0,0 +1,168 @@
<template>
<v-dialog fullscreen hide-overlay scrollable lazy
v-model="show" persistent class="p-upload-dialog" @keydown.esc="cancel">
<v-card color="application">
<v-toolbar dark color="navigation">
<v-btn icon dark @click.stop="cancel" :disabled="busy">
<v-icon>close</v-icon>
</v-btn>
<v-toolbar-title><translate>Upload</translate></v-toolbar-title>
</v-toolbar>
<v-container grid-list-xs text-xs-left fluid>
<v-form ref="form" class="p-photo-upload" lazy-validation @submit.prevent="submit" dense>
<input type="file" ref="upload" multiple @change.stop="upload()" class="d-none">
<v-container fluid>
<p class="subheading">
<span v-if="total === 0">Select photos to start upload...</span>
<span v-else-if="failed">Upload failed</span>
<span v-else-if="total > 0 && completed < 100">
Uploading {{current}} of {{total}}...
</span>
<span v-else-if="indexing">Upload complete. Indexing...</span>
<span v-else-if="completed === 100">Done.</span>
</p>
<v-progress-linear color="secondary-dark" v-model="completed"
:indeterminate="indexing"></v-progress-linear>
<p class="subheading" v-if="safe">
Please don't upload photos containing offensive content. Uploads
that may contain such images will be rejected automatically.
</p>
<v-btn
:disabled="busy"
color="secondary-dark"
class="white--text ml-0 mt-2"
depressed
@click.stop="uploadDialog()"
>
<translate>Upload</translate>
<v-icon right dark>cloud_upload</v-icon>
</v-btn>
</v-container>
</v-form>
</v-container>
</v-card>
</v-dialog>
</template>
<script>
import Api from "common/api";
import Notify from "common/notify";
export default {
name: 'p-tab-upload',
props: {
show: Boolean,
},
data() {
return {
selected: [],
uploads: [],
busy: false,
indexing: false,
failed: false,
current: 0,
total: 0,
completed: 0,
started: 0,
safe: !this.$config.getValue("uploadNSFW")
}
},
methods: {
cancel() {
this.$emit('cancel');
},
confirm() {
this.$emit('confirm');
},
submit() {
// DO NOTHING
},
uploadDialog() {
this.$refs.upload.click();
},
upload() {
this.started = Date.now();
this.selected = this.$refs.upload.files;
this.busy = true;
this.indexing = false;
this.failed = false;
this.total = this.selected.length;
this.current = 0;
this.completed = 0;
this.uploads = [];
if (!this.total) {
return
}
Notify.info(this.$gettext("Uploading photos..."));
Notify.blockUI();
async function performUpload(ctx) {
for (let i = 0; i < ctx.selected.length; i++) {
let file = ctx.selected[i];
let formData = new FormData();
ctx.current = i + 1;
formData.append('files', file);
await Api.post('upload/' + ctx.started,
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
).then(() => {
ctx.completed = Math.round((ctx.current / ctx.total) * 100);
}).catch(() => {
ctx.busy = false;
ctx.indexing = false;
ctx.completed = 100;
ctx.failed = true;
Notify.unblockUI();
throw Error("upload failed");
});
}
}
performUpload(this).then(() => {
this.indexing = true;
const ctx = this;
Api.post('import/upload/' + this.started).then(() => {
Notify.unblockUI();
Notify.success(ctx.$gettext("Upload complete"));
ctx.busy = false;
ctx.indexing = false;
ctx.$emit('confirm');
}).catch(() => {
Notify.unblockUI();
Notify.error(ctx.$gettext("Failure while importing uploaded files"));
ctx.busy = false;
ctx.indexing = false;
});
});
},
},
watch: {
show: function () {
this.selected = [];
this.uploads = [];
this.busy = false;
this.indexing = false;
this.failed = false;
this.current = 0;
this.total = 0;
this.completed = 0;
this.started = 0;
}
},
};
</script>

View file

@ -19,7 +19,7 @@
<v-spacer></v-spacer>
<v-btn icon @click.stop="refresh">
<v-btn icon @click.stop="refresh" :class="dirty ? 'secondary-light': ''">
<v-icon>refresh</v-icon>
</v-btn>
@ -160,6 +160,7 @@
return {
subId: null,
dirty: false,
results: [],
loading: true,
scrollDisabled: true,
@ -261,6 +262,7 @@
Album.search(params).then(response => {
this.loading = false;
this.dirty = false;
this.results = response.models;
this.scrollDisabled = (response.models.length < this.pageSize);
@ -318,7 +320,11 @@
}
},
onCount() {
// TODO
this.dirty = true;
if(!this.selection && this.offset === 0) {
this.refresh();
}
}
},
created() {

View file

@ -22,12 +22,12 @@
<p-tab-import></p-tab-import>
</v-tab-item>
<v-tab id="tab-upload" :disabled="readonly" ripple @click="changePath('/library/upload')">
<!-- v-tab id="tab-upload" :disabled="readonly" ripple @click="changePath('/library/upload')">
<translate>Upload</translate>
</v-tab>
<v-tab-item :disabled="readonly">
<p-tab-upload></p-tab-upload>
</v-tab-item>
</v-tab-item -->
<v-tab id="tab-logs" ripple @click="changePath('/library/logs')">
<translate>Logs</translate>
@ -40,7 +40,7 @@
</template>
<script>
import uploadTab from "pages/library/upload.vue";
// import uploadTab from "pages/library/upload.vue";
import importTab from "pages/library/import.vue";
import originalsTab from "pages/library/originals.vue";
import tabLogs from "pages/library/logs.vue";
@ -53,7 +53,7 @@
components: {
'p-tab-originals': originalsTab,
'p-tab-import': importTab,
'p-tab-upload': uploadTab,
// 'p-tab-upload': uploadTab,
'p-tab-logs': tabLogs,
},
data() {

View file

@ -35,13 +35,13 @@
:disabled="busy"
:label="labels.createThumbs"
></v-checkbox>
<v-checkbox
<!-- v-checkbox
class="ma-0 pa-0"
v-model="options.groomMetadata"
color="secondary-dark"
:disabled="busy"
:label="labels.groomMetadata"
></v-checkbox>
></v-checkbox -->
<v-btn
:disabled="!busy"

View file

@ -2,7 +2,7 @@
<div class="p-page p-page-photos" v-infinite-scroll="loadMore" :infinite-scroll-disabled="scrollDisabled"
:infinite-scroll-distance="10" :infinite-scroll-listen-for-event="'scrollRefresh'">
<p-photo-search :settings="settings" :filter="filter" :filter-change="updateQuery"
<p-photo-search :settings="settings" :filter="filter" :filter-change="updateQuery" :dirty="dirty"
:refresh="refresh"></p-photo-search>
<v-container fluid class="pa-4" v-if="loading">
@ -33,6 +33,7 @@
<script>
import Photo from "model/photo";
import Event from "pubsub-js";
export default {
name: 'p-page-photos',
@ -83,6 +84,9 @@
const settings = {view: view};
return {
uploadSubId: null,
countSubId: null,
dirty: false,
results: [],
scrollDisabled: true,
pageSize: 60,
@ -221,6 +225,7 @@
Photo.search(params).then(response => {
this.loading = false;
this.dirty = false;
this.results = response.models;
this.scrollDisabled = (response.models.length < this.pageSize);
@ -240,11 +245,29 @@
}
}).catch(() => this.loading = false);
},
onImportCompleted() {
this.dirty = true;
console.log("onImportCompleted", this.selection, this.offset);
if(this.selection.length === 0 && this.offset === 0) {
console.log("REFRESH");
this.refresh();
}
},
onCount() {
this.dirty = true;
}
},
created() {
this.search();
this.uploadSubId = Event.subscribe("import.completed", (ev, data) => this.onImportCompleted(ev, data));
this.countSubId = Event.subscribe("count.photos", (ev, data) => this.onCount(ev, data));
},
destroyed() {
Event.unsubscribe(this.uploadSubId);
Event.unsubscribe(this.countSubId);
}
};
</script>

View file

@ -9,7 +9,7 @@
"accent": "#9E9E9E",
"error": "#E57373",
"info": "#0097A7",
"success": "#00BFA5",
"success": "#00897B",
"warning": "#FFE082",
"remove": "#E57373",
"restore": "#64B5F6",
@ -32,7 +32,7 @@
"accent": "#757575",
"error": "#E57373",
"info": "#0097A7",
"success": "#00BFA5",
"success": "#00897B",
"warning": "#FFE082",
"remove": "#E57373",
"restore": "#64B5F6",
@ -55,7 +55,7 @@
"accent": "#9E9E9E",
"error": "#E57373",
"info": "#0097A7",
"success": "#00BFA5",
"success": "#00897B",
"warning": "#FFE082",
"remove": "#E57373",
"restore": "#64B5F6",
@ -78,7 +78,7 @@
"accent": "#B0BEC5",
"error": "#E57373",
"info": "#0097A7",
"success": "#00BFA5",
"success": "#00897B",
"warning": "#FFE082",
"remove": "#E57373",
"restore": "#64B5F6",
@ -101,7 +101,7 @@
"accent": "#B0BEC5",
"error": "#E57373",
"info": "#0097A7",
"success": "#00BFA5",
"success": "#00897B",
"warning": "#FFE082",
"remove": "#E57373",
"restore": "#64B5F6",
@ -124,7 +124,7 @@
"accent": "#757575",
"error": "#E57373",
"info": "#0097A7",
"success": "#00BFA5",
"success": "#00897B",
"warning": "#FFE082",
"remove": "#E57373",
"restore": "#64B5F6",

View file

@ -99,13 +99,6 @@ export default [
path: "/library/logs",
component: Library,
meta: {title: "Server Logs", auth: true, background: "application-light"},
props: {tab: 3},
},
{
name: "library_upload",
path: "/library/upload",
component: Library,
meta: {title: "Photo Upload", auth: true, background: "application-light"},
props: {tab: 2},
},
{

View file

@ -9,6 +9,7 @@ import (
"time"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/gin-gonic/gin"
@ -37,6 +38,8 @@ func Upload(router *gin.RouterGroup, conf *config.Config) {
return
}
event.Publish("upload.start", event.Data{"time": start})
files := f.File["files"]
uploaded := len(files)
var uploads []string