Upload: Refactor UX
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
b3a50695c0
commit
777526ce82
11 changed files with 252 additions and 28 deletions
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
168
frontend/src/dialog/p-upload-dialog.vue
Normal file
168
frontend/src/dialog/p-upload-dialog.vue
Normal 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>
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue