Implemented detail view proof-of-concept for photo search

This commit is contained in:
Michael Mayer 2018-09-19 20:15:46 +02:00
parent d7e4531231
commit 4ac3b2a602
6 changed files with 84 additions and 16 deletions

View file

@ -75,6 +75,7 @@
"vue-router": "^2.7.0", "vue-router": "^2.7.0",
"vue-style-loader": "^2.0.0", "vue-style-loader": "^2.0.0",
"vue-template-compiler": "^2.4.4", "vue-template-compiler": "^2.4.4",
"vue-truncate-filter": "^1.1.7",
"vuelidate": "^0.4.3", "vuelidate": "^0.4.3",
"vuetify": "^1.2.3", "vuetify": "^1.2.3",
"webpack": "^3.12.0", "webpack": "^3.12.0",

View file

@ -12,6 +12,7 @@ import Session from 'common/session';
import Event from 'pubsub-js'; import Event from 'pubsub-js';
import Moment from 'vue-moment'; import Moment from 'vue-moment';
import InfiniteScroll from 'vue-infinite-scroll'; import InfiniteScroll from 'vue-infinite-scroll';
import VueTruncate from 'vue-truncate-filter';
const session = new Session(window.localStorage); const session = new Session(window.localStorage);
const config = new Config(window.localStorage, window.appConfig); const config = new Config(window.localStorage, window.appConfig);
@ -37,6 +38,7 @@ Vue.use(Vuetify, {
Vue.use(Moment); Vue.use(Moment);
Vue.use(InfiniteScroll); Vue.use(InfiniteScroll);
Vue.use(VueTruncate);
Vue.use(AppComponents); Vue.use(AppComponents);
Vue.use(Router); Vue.use(Router);

View file

@ -160,6 +160,59 @@
</template> </template>
</v-data-table> </v-data-table>
<v-container grid-list-xs fluid class="pa-0" v-if="query.view === 'details'">
<v-card v-if="results.length === 0">
<v-card-title primary-title>
<div>
<h3 class="headline mb-3">No photos matched your search</h3>
<div>Try using other terms and search options such as category, country and camera.</div>
</div>
</v-card-title>
</v-card>
<v-layout row wrap>
<v-flex
v-for="photo in results"
:key="photo.ID"
xs12 sm6 md4 lg3 d-flex
>
<v-card tile class="ma-2">
<v-img
:src="'/api/v1/files/' + photo.FileHash + '/square_thumbnail?size=500'"
aspect-ratio="1"
v-bind:class="{ selected: photo.selected }"
@click="selectPhoto(photo)"
>
<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-title primary-title class="pa-3">
<div>
<h3 class="subheading text-truncate mb-0">{{ photo.PhotoTitle | truncate(50)}}</h3>
<div><v-icon small>date_range</v-icon> {{ photo.TakenAt | moment('DD/MM/YYYY hh:mm:ss') }}
<v-spacer></v-spacer>
<v-icon small>photo_camera</v-icon> {{ photo.CameraModel }}<v-spacer></v-spacer>
<v-icon small>location_on</v-icon> {{ photo.LocName ? photo.LocName + ', ' : ''}}{{ photo.LocCity ? photo.LocCity + ', ' : ''}}{{ photo.LocCounty ? photo.LocCounty + ', ' : ''}}{{ photo.LocCountry }}
</div>
</div>
</v-card-title>
<!-- v-card-actions>
<v-btn flat color="orange">Like</v-btn>
<v-btn flat color="orange">Edit</v-btn>
</v-card-actions -->
</v-card>
</v-flex>
</v-layout>
</v-container>
<v-container grid-list-xs fluid class="pa-0" v-if="query.view === 'tiles'"> <v-container grid-list-xs fluid class="pa-0" v-if="query.view === 'tiles'">
<v-card v-if="results.length === 0"> <v-card v-if="results.length === 0">
@ -181,7 +234,7 @@
<v-tooltip bottom> <v-tooltip bottom>
<v-card-actions flat tile class="d-flex" slot="activator" @click="selectPhoto(photo)" <v-card-actions flat tile class="d-flex" slot="activator" @click="selectPhoto(photo)"
@mouseover="overPhoto(photo)" @mouseleave="leavePhoto(photo)"> @mouseover="overPhoto(photo)" @mouseleave="leavePhoto(photo)">
<v-img :src="'/api/v1/files/' + photo.FileID + '/square_thumbnail?size=500'" <v-img :src="'/api/v1/files/' + photo.FileHash + '/square_thumbnail?size=500'"
aspect-ratio="1" aspect-ratio="1"
class="grey lighten-2" class="grey lighten-2"
> >
@ -233,7 +286,7 @@
const camera = query['camera'] ? parseInt(query['camera']) : 0; const camera = query['camera'] ? parseInt(query['camera']) : 0;
const q = query['q'] ? query['q'] : ''; const q = query['q'] ? query['q'] : '';
const country = query['country'] ? query['country'] : ''; const country = query['country'] ? query['country'] : '';
const view = query['view'] === 'list' ? 'list' : 'tiles'; const view = query['view'] ? query['view'] : 'tiles';
const cameras = [{ID: 0, CameraModel: 'All Cameras'}].concat(this.$config.getValue('cameras')); const cameras = [{ID: 0, CameraModel: 'All Cameras'}].concat(this.$config.getValue('cameras'));
const countries = [{ const countries = [{
LocCountryCode: '', LocCountryCode: '',
@ -264,6 +317,7 @@
], ],
'views': [ 'views': [
{value: 'tiles', text: 'Tiles'}, {value: 'tiles', text: 'Tiles'},
{value: 'details', text: 'Details'},
{value: 'list', text: 'List'}, {value: 'list', text: 'List'},
], ],
'countries': countries, 'countries': countries,
@ -275,12 +329,12 @@
], ],
}, },
'listColumns': [ 'listColumns': [
{ text: 'Title', value: 'PhotoTitle' }, {text: 'Title', value: 'PhotoTitle'},
{ text: 'Taken At', value: 'TakenAt' }, {text: 'Taken At', value: 'TakenAt'},
{ text: 'City', value: 'LocCity' }, {text: 'City', value: 'LocCity'},
{ text: 'Country', value: 'LocCountry' }, {text: 'Country', value: 'LocCountry'},
{ text: 'Camera', value: 'CameraModel' }, {text: 'Camera', value: 'CameraModel'},
{ text: 'Favorite', value: 'PhotoFavorite' }, {text: 'Favorite', value: 'PhotoFavorite'},
], ],
'view': view, 'view': view,
'loadMoreDisabled': true, 'loadMoreDisabled': true,

View file

@ -6987,6 +6987,10 @@ vue-template-es2015-compiler@^1.2.2:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18" resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18"
vue-truncate-filter@^1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/vue-truncate-filter/-/vue-truncate-filter-1.1.7.tgz#e365fd2d4520c017293308e0ecbb1dd6176c3af2"
vue@^2.4.4: vue@^2.4.4:
version "2.5.16" version "2.5.16"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.16.tgz#07edb75e8412aaeed871ebafa99f4672584a0085" resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.16.tgz#07edb75e8412aaeed871ebafa99f4672584a0085"

View file

@ -57,6 +57,7 @@ type PhotoSearchResult struct {
// File // File
FileID uint FileID uint
FileName string FileName string
FileHash string
FileType string FileType string
FileMime string FileMime string
FileWidth int FileWidth int
@ -81,7 +82,7 @@ func (s *Search) Photos(form PhotoSearchForm) ([]PhotoSearchResult, error) {
q := s.db.NewScope(nil).DB() q := s.db.NewScope(nil).DB()
q = q.Table("photos"). q = q.Table("photos").
Select(`SQL_CALC_FOUND_ROWS photos.*, Select(`SQL_CALC_FOUND_ROWS photos.*,
files.id AS file_id, files.file_name, files.file_type, files.file_mime, files.file_width, files.file_height, files.file_aspect_ratio, files.file_orientation, files.id AS file_id, files.file_name, files.file_hash, files.file_type, files.file_mime, files.file_width, files.file_height, files.file_aspect_ratio, files.file_orientation,
cameras.camera_model, cameras.camera_model,
locations.loc_display_name, locations.loc_name, locations.loc_city, locations.loc_postcode, locations.loc_country, locations.loc_country_code, locations.loc_category, locations.loc_type, locations.loc_display_name, locations.loc_name, locations.loc_city, locations.loc_postcode, locations.loc_country, locations.loc_country_code, locations.loc_category, locations.loc_type,
GROUP_CONCAT(tags.tag_label) AS tags`). GROUP_CONCAT(tags.tag_label) AS tags`).
@ -165,8 +166,14 @@ func (s *Search) FindFiles(count int, offset int) (files []File) {
return files return files
} }
func (s *Search) FindFile(id string) (file File) { func (s *Search) FindFileById(id string) (file File) {
s.db.Where("id = ?", id).First(&file) s.db.Where("id = ?", id).First(&file)
return file return file
} }
func (s *Search) FindFileByHash(fileHash string) (file File) {
s.db.Where("file_hash = ?", fileHash).First(&file)
return file
}

View file

@ -50,13 +50,13 @@ func ConfigureRoutes(app *gin.Engine, conf *photoprism.Config) {
c.JSON(http.StatusOK, files) c.JSON(http.StatusOK, files)
}) })
v1.GET("/files/:id/thumbnail", func(c *gin.Context) { v1.GET("/files/:hash/thumbnail", func(c *gin.Context) {
id := c.Param("id") fileHash := c.Param("hash")
size, _ := strconv.Atoi(c.Query("size")) size, _ := strconv.Atoi(c.Query("size"))
search := photoprism.NewSearch(conf.OriginalsPath, conf.GetDb()) search := photoprism.NewSearch(conf.OriginalsPath, conf.GetDb())
file := search.FindFile(id) file := search.FindFileByHash(fileHash)
fileName := fmt.Sprintf("%s/%s", conf.OriginalsPath, file.FileName) fileName := fmt.Sprintf("%s/%s", conf.OriginalsPath, file.FileName)
@ -69,13 +69,13 @@ func ConfigureRoutes(app *gin.Engine, conf *photoprism.Config) {
} }
}) })
v1.GET("/files/:id/square_thumbnail", func(c *gin.Context) { v1.GET("/files/:hash/square_thumbnail", func(c *gin.Context) {
id := c.Param("id") fileHash := c.Param("hash")
size, _ := strconv.Atoi(c.Query("size")) size, _ := strconv.Atoi(c.Query("size"))
search := photoprism.NewSearch(conf.OriginalsPath, conf.GetDb()) search := photoprism.NewSearch(conf.OriginalsPath, conf.GetDb())
file := search.FindFile(id) file := search.FindFileByHash(fileHash)
fileName := fmt.Sprintf("%s/%s", conf.OriginalsPath, file.FileName) fileName := fmt.Sprintf("%s/%s", conf.OriginalsPath, file.FileName)