mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 08:44:04 +01:00
Rename "Import" to "Library" and add tabs to page
This commit is contained in:
@@ -165,13 +165,13 @@
|
|||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
</v-list-group>
|
</v-list-group>
|
||||||
|
|
||||||
<v-list-tile to="/import" @click="">
|
<v-list-tile to="/library" @click="">
|
||||||
<v-list-tile-action>
|
<v-list-tile-action>
|
||||||
<v-icon>camera_roll</v-icon>
|
<v-icon>camera_roll</v-icon>
|
||||||
</v-list-tile-action>
|
</v-list-tile-action>
|
||||||
|
|
||||||
<v-list-tile-content>
|
<v-list-tile-content>
|
||||||
<v-list-tile-title>Import</v-list-tile-title>
|
<v-list-tile-title>Library</v-list-tile-title>
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ main {
|
|||||||
color: #00B8D4;
|
color: #00B8D4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#photoprism main a.v-tabs__item {
|
||||||
|
color: #546E7A;
|
||||||
|
}
|
||||||
|
|
||||||
|
#photoprism main a.v-tabs__item--active {
|
||||||
|
color: #455A64;
|
||||||
|
}
|
||||||
|
|
||||||
#photoprism .v-badge__badge {
|
#photoprism .v-badge__badge {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
height: 19px;
|
height: 19px;
|
||||||
|
|||||||
54
frontend/src/pages/library.vue
Normal file
54
frontend/src/pages/library.vue
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-page p-page-library">
|
||||||
|
<v-tabs
|
||||||
|
v-model="active"
|
||||||
|
flat
|
||||||
|
grow
|
||||||
|
color="blue-grey lighten-4"
|
||||||
|
slider-color="blue-grey darken-1"
|
||||||
|
height="64"
|
||||||
|
>
|
||||||
|
<v-tab id="tab-upload" ripple>
|
||||||
|
Upload
|
||||||
|
</v-tab>
|
||||||
|
<v-tab-item>
|
||||||
|
<p-tab-upload></p-tab-upload>
|
||||||
|
</v-tab-item>
|
||||||
|
|
||||||
|
<v-tab id="tab-import" ripple>
|
||||||
|
Import
|
||||||
|
</v-tab>
|
||||||
|
<v-tab-item>
|
||||||
|
<p-tab-import></p-tab-import>
|
||||||
|
</v-tab-item>
|
||||||
|
|
||||||
|
<v-tab id="tab-index" ripple>
|
||||||
|
Index
|
||||||
|
</v-tab>
|
||||||
|
<v-tab-item>
|
||||||
|
<p-tab-index></p-tab-index>
|
||||||
|
</v-tab-item>
|
||||||
|
</v-tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import uploadTab from "pages/library/upload.vue";
|
||||||
|
import importTab from "pages/library/import.vue";
|
||||||
|
import indexTab from "pages/library/index.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'p-page-library',
|
||||||
|
components: {
|
||||||
|
'p-tab-upload': uploadTab,
|
||||||
|
'p-tab-import': importTab,
|
||||||
|
'p-tab-index': indexTab,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
active: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
35
frontend/src/pages/library/import.vue
Normal file
35
frontend/src/pages/library/import.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-tab p-tab-import">
|
||||||
|
<v-form ref="form" class="p-photo-import" lazy-validation @submit.prevent="submit" dense>
|
||||||
|
<input type="file" ref="upload" multiple @change.stop="upload()" class="d-none">
|
||||||
|
|
||||||
|
<v-container fluid>
|
||||||
|
<h3 class="subheading">Only available using the command-line interface at the moment</h3>
|
||||||
|
<p class="body-1 mt-2">
|
||||||
|
Issues labeled <a href="https://github.com/photoprism/photoprism/labels/help%20wanted">help
|
||||||
|
wanted</a> /
|
||||||
|
<a href="https://github.com/photoprism/photoprism/labels/easy">easy</a> can be good (first)
|
||||||
|
contributions.
|
||||||
|
Our <a href="https://github.com/photoprism/photoprism/wiki">Developer Guide</a> contains all
|
||||||
|
information
|
||||||
|
necessary to get you started.
|
||||||
|
</p>
|
||||||
|
</v-container>
|
||||||
|
</v-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from "axios";
|
||||||
|
import Event from "pubsub-js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'p-tab-import',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
35
frontend/src/pages/library/index.vue
Normal file
35
frontend/src/pages/library/index.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-tab p-tab-index">
|
||||||
|
<v-form ref="form" class="p-photo-index" lazy-validation @submit.prevent="submit" dense>
|
||||||
|
<input type="file" ref="upload" multiple @change.stop="upload()" class="d-none">
|
||||||
|
|
||||||
|
<v-container fluid>
|
||||||
|
<h3 class="subheading">Only available using the command-line interface at the moment</h3>
|
||||||
|
<p class="body-1 mt-2">
|
||||||
|
Issues labeled <a href="https://github.com/photoprism/photoprism/labels/help%20wanted">help
|
||||||
|
wanted</a> /
|
||||||
|
<a href="https://github.com/photoprism/photoprism/labels/easy">easy</a> can be good (first)
|
||||||
|
contributions.
|
||||||
|
Our <a href="https://github.com/photoprism/photoprism/wiki">Developer Guide</a> contains all
|
||||||
|
information
|
||||||
|
necessary to get you started.
|
||||||
|
</p>
|
||||||
|
</v-container>
|
||||||
|
</v-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from "axios";
|
||||||
|
import Event from "pubsub-js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'p-tab-index',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,56 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-page p-page-import">
|
<div class="p-tab p-tab-upload">
|
||||||
<v-form ref="form" class="p-photo-import" lazy-validation @submit.prevent="submit" dense>
|
<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">
|
<input type="file" ref="upload" multiple @change.stop="upload()" class="d-none">
|
||||||
|
|
||||||
<v-toolbar flat color="blue-grey lighten-4">
|
|
||||||
<v-toolbar-title>Import</v-toolbar-title>
|
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-toolbar>
|
|
||||||
|
|
||||||
<v-container fluid>
|
<v-container fluid>
|
||||||
<p class="subheading">
|
<p class="subheading">
|
||||||
<span v-if="total === 0">Select photos to start import...</span>
|
<span v-if="total === 0">Select photos to start upload...</span>
|
||||||
<span v-else-if="total > 0 && completed < 100">Adding {{current}} of {{total}}...</span>
|
<span v-else-if="total > 0 && completed < 100">Uploaded {{current}} of {{total}}...</span>
|
||||||
<span v-else-if="completed === 100">Done.</span>
|
<span v-else-if="completed === 100">Done.</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<v-progress-linear color="blue-grey" v-model="completed"></v-progress-linear>
|
<v-progress-linear color="blue-grey" v-model="completed"></v-progress-linear>
|
||||||
|
|
||||||
<v-container grid-list-xs fluid class="pa-0 p-photos p-photo-mosaic">
|
|
||||||
<v-layout row wrap>
|
|
||||||
<v-flex
|
|
||||||
v-for="(file, index) in uploads"
|
|
||||||
:key="index"
|
|
||||||
class="p-photo"
|
|
||||||
xs4 sm3 md2 lg1 d-flex
|
|
||||||
>
|
|
||||||
<v-card tile class="elevation-2 ma-2">
|
|
||||||
<v-img :src="file.data"
|
|
||||||
aspect-ratio="1"
|
|
||||||
:title="file.name"
|
|
||||||
class="grey lighten-2"
|
|
||||||
>
|
|
||||||
<v-layout
|
|
||||||
slot="placeholder"
|
|
||||||
fill-height
|
|
||||||
align-center
|
|
||||||
justify-center
|
|
||||||
ma-0
|
|
||||||
>
|
|
||||||
<v-progress-circular indeterminate
|
|
||||||
color="grey lighten-5"></v-progress-circular>
|
|
||||||
</v-layout>
|
|
||||||
</v-img>
|
|
||||||
</v-card>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
:disabled="busy"
|
:disabled="busy"
|
||||||
color="blue-grey lighten-2"
|
color="blue-grey"
|
||||||
class="white--text ml-0"
|
class="white--text ml-0"
|
||||||
depressed
|
depressed
|
||||||
@click.stop="uploadDialog()"
|
@click.stop="uploadDialog()"
|
||||||
@@ -68,7 +32,7 @@
|
|||||||
import Event from "pubsub-js";
|
import Event from "pubsub-js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'p-page-import',
|
name: 'p-tab-upload',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selected: [],
|
selected: [],
|
||||||
@@ -77,6 +41,7 @@
|
|||||||
current: 0,
|
current: 0,
|
||||||
total: 0,
|
total: 0,
|
||||||
completed: 0,
|
completed: 0,
|
||||||
|
started: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -87,6 +52,7 @@
|
|||||||
this.$refs.upload.click();
|
this.$refs.upload.click();
|
||||||
},
|
},
|
||||||
upload() {
|
upload() {
|
||||||
|
this.started = Date.now();
|
||||||
this.selected = this.$refs.upload.files;
|
this.selected = this.$refs.upload.files;
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
this.total = this.selected.length;
|
this.total = this.selected.length;
|
||||||
@@ -94,7 +60,7 @@
|
|||||||
this.completed = 0;
|
this.completed = 0;
|
||||||
this.uploads = [];
|
this.uploads = [];
|
||||||
|
|
||||||
if(!this.total) {
|
if (!this.total) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,24 +70,12 @@
|
|||||||
|
|
||||||
async function performUpload(ctx) {
|
async function performUpload(ctx) {
|
||||||
for (let i = 0; i < ctx.selected.length; i++) {
|
for (let i = 0; i < ctx.selected.length; i++) {
|
||||||
ctx.current = i + 1;
|
|
||||||
ctx.completed = Math.round((ctx.current / ctx.total) * 100);
|
|
||||||
let file = ctx.selected[i];
|
let file = ctx.selected[i];
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
|
|
||||||
formData.append('files', file);
|
formData.append('files', file);
|
||||||
|
|
||||||
if (file.type.match('image.*')) {
|
await axios.post('/api/v1/upload/' + ctx.started,
|
||||||
const reader = new FileReader;
|
|
||||||
|
|
||||||
reader.onload = e => {
|
|
||||||
ctx.uploads.push({name: file.name, data: e.target.result});
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.readAsDataURL(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
await axios.post('/api/v1/upload',
|
|
||||||
formData,
|
formData,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
@@ -129,6 +83,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
).then(function () {
|
).then(function () {
|
||||||
|
ctx.current = i + 1;
|
||||||
|
ctx.completed = Math.round((ctx.current / ctx.total) * 100);
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
Event.publish("alert.error", "Upload failed");
|
Event.publish("alert.error", "Upload failed");
|
||||||
});
|
});
|
||||||
@@ -136,8 +92,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
performUpload(this).then(() => {
|
performUpload(this).then(() => {
|
||||||
|
axios.post('/api/v1/import/upload/' + this.started).then(function () {
|
||||||
|
Event.publish("alert.success", "Finished indexing upload");
|
||||||
|
});
|
||||||
|
|
||||||
Event.publish("ajax.end");
|
Event.publish("ajax.end");
|
||||||
Event.publish("alert.success", "Photos uploaded and imported");
|
Event.publish("alert.success", "Upload completed");
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -3,7 +3,7 @@ import Places from "pages/places.vue";
|
|||||||
import Labels from "pages/labels.vue";
|
import Labels from "pages/labels.vue";
|
||||||
import Events from "pages/events.vue";
|
import Events from "pages/events.vue";
|
||||||
import People from "pages/people.vue";
|
import People from "pages/people.vue";
|
||||||
import Import from "pages/import.vue";
|
import Library from "pages/library.vue";
|
||||||
import Share from "pages/share.vue";
|
import Share from "pages/share.vue";
|
||||||
import Settings from "pages/settings.vue";
|
import Settings from "pages/settings.vue";
|
||||||
import Todo from "pages/todo.vue";
|
import Todo from "pages/todo.vue";
|
||||||
@@ -64,10 +64,10 @@ export default [
|
|||||||
meta: {area: "Albums"},
|
meta: {area: "Albums"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Import",
|
name: "Library",
|
||||||
path: "/import",
|
path: "/library",
|
||||||
component: Import,
|
component: Library,
|
||||||
meta: {area: "Import"},
|
meta: {area: "Library"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Share",
|
name: "Share",
|
||||||
|
|||||||
52
internal/api/import.go
Normal file
52
internal/api/import.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
|
)
|
||||||
|
|
||||||
|
var importer *photoprism.Importer
|
||||||
|
|
||||||
|
func initImporter(conf *config.Config) {
|
||||||
|
if importer != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tensorFlow := photoprism.NewTensorFlow(conf)
|
||||||
|
|
||||||
|
indexer := photoprism.NewIndexer(conf, tensorFlow)
|
||||||
|
|
||||||
|
converter := photoprism.NewConverter(conf)
|
||||||
|
|
||||||
|
importer = photoprism.NewImporter(conf, indexer, converter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/v1/import
|
||||||
|
func Import(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
router.POST("/import/*path", func(c *gin.Context) {
|
||||||
|
start := time.Now()
|
||||||
|
path := conf.ImportPath()
|
||||||
|
|
||||||
|
if subPath := c.Param("path"); subPath != "" {
|
||||||
|
log.Debugf("import sub path: %s", subPath)
|
||||||
|
path = path + subPath
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("importing photos from %s", path)
|
||||||
|
|
||||||
|
initImporter(conf)
|
||||||
|
|
||||||
|
importer.ImportPhotosFromDirectory(path)
|
||||||
|
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("completed import in %s", elapsed)})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -9,33 +9,16 @@ import (
|
|||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
uuid "github.com/satori/go.uuid"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var importer *photoprism.Importer
|
// POST /api/v1/upload/:path
|
||||||
|
|
||||||
func initImporter(conf *config.Config) {
|
|
||||||
if importer != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tensorFlow := photoprism.NewTensorFlow(conf)
|
|
||||||
|
|
||||||
indexer := photoprism.NewIndexer(conf, tensorFlow)
|
|
||||||
|
|
||||||
converter := photoprism.NewConverter(conf)
|
|
||||||
|
|
||||||
importer = photoprism.NewImporter(conf, indexer, converter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/v1/upload
|
|
||||||
func Upload(router *gin.RouterGroup, conf *config.Config) {
|
func Upload(router *gin.RouterGroup, conf *config.Config) {
|
||||||
router.POST("/upload", func(c *gin.Context) {
|
router.POST("/upload/:path", func(c *gin.Context) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
subPath := c.Param("path")
|
||||||
|
|
||||||
form, err := c.MultipartForm()
|
form, err := c.MultipartForm()
|
||||||
|
|
||||||
@@ -49,7 +32,7 @@ func Upload(router *gin.RouterGroup, conf *config.Config) {
|
|||||||
|
|
||||||
files := form.File["files"]
|
files := form.File["files"]
|
||||||
|
|
||||||
path := fmt.Sprintf("%s/uploads/%s", conf.ImportPath(), uuid.NewV4())
|
path := fmt.Sprintf("%s/upload/%s", conf.ImportPath(), subPath)
|
||||||
|
|
||||||
if err := os.MkdirAll(path, os.ModePerm); err != nil {
|
if err := os.MkdirAll(path, os.ModePerm); err != nil {
|
||||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())})
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())})
|
||||||
@@ -65,16 +48,10 @@ func Upload(router *gin.RouterGroup, conf *config.Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("importing photos from %s", conf.ImportPath())
|
|
||||||
|
|
||||||
initImporter(conf)
|
|
||||||
|
|
||||||
importer.ImportPhotosFromDirectory(conf.ImportPath())
|
|
||||||
|
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
log.Infof("%d files imported in %s", len(files), elapsed)
|
log.Infof("%d files uploaded in %s", len(files), elapsed)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("%d files imported in %s", len(files), elapsed)})
|
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("%d files uploaded in %s", len(files), elapsed)})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||||||
api.LabelThumbnail(v1, conf)
|
api.LabelThumbnail(v1, conf)
|
||||||
|
|
||||||
api.Upload(v1, conf)
|
api.Upload(v1, conf)
|
||||||
|
api.Import(v1, conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default HTML page (client-side routing implemented via Vue.js)
|
// Default HTML page (client-side routing implemented via Vue.js)
|
||||||
|
|||||||
Reference in New Issue
Block a user