UX: Rename "Videos" to "Media" in navigation and add audio type #4694

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-03-26 12:53:15 +01:00
parent 6f08d7f5b9
commit b0eb7aacdd
60 changed files with 11063 additions and 10455 deletions

View File

@@ -222,11 +222,18 @@ export default [
meta: { title: $gettext("Favorites"), requiresAuth: true },
props: { staticFilter: { favorite: "true" } },
},
{
name: "media",
path: "/media",
component: Photos,
meta: { title: $gettext("Media"), requiresAuth: true },
props: { staticFilter: { media: "true" } },
},
{
name: "live",
path: "/live",
component: Photos,
meta: { title: $gettext("Live"), requiresAuth: true },
meta: { title: $gettext("Live Photos"), requiresAuth: true },
props: { staticFilter: { live: "true" } },
},
{
@@ -236,6 +243,20 @@ export default [
meta: { title: $gettext("Videos"), requiresAuth: true },
props: { staticFilter: { video: "true" } },
},
{
name: "audio",
path: "/audio",
component: Photos,
meta: { title: $gettext("Audio"), requiresAuth: true },
props: { staticFilter: { audio: "true" } },
},
{
name: "animated",
path: "/animated",
component: Photos,
meta: { title: $gettext("Animated"), requiresAuth: true },
props: { staticFilter: { animated: "true" } },
},
{
name: "review",
path: "/review",

View File

@@ -291,14 +291,30 @@ export default class Config {
this.values.count.all += data.count;
this.values.count.photos += data.count;
break;
case "live":
case "animated":
this.values.count.all += data.count;
this.values.count.live += data.count;
this.values.count.media += data.count;
this.values.count.animated += data.count;
break;
case "videos":
this.values.count.all += data.count;
this.values.count.media += data.count;
this.values.count.videos += data.count;
break;
case "live":
this.values.count.all += data.count;
this.values.count.media += data.count;
this.values.count.live += data.count;
break;
case "audio":
this.values.count.all += data.count;
this.values.count.media += data.count;
this.values.count.audio += data.count;
break;
case "documents":
this.values.count.all += data.count;
this.values.count.documents += data.count;
break;
case "cameras":
this.values.count.cameras += data.count;
this.update();

View File

@@ -156,18 +156,6 @@
</v-list-item-title>
</v-list-item>
<v-list-item
:to="{ name: 'browse', query: { q: 'animated' } }"
:exact="true"
variant="text"
class="nav-animated"
@click.stop=""
>
<v-list-item-title :class="`nav-menu-item menu-item`">
{{ $gettext(`Animated`) }}
</v-list-item-title>
</v-list-item>
<v-list-item
:to="{ name: 'photos', query: { q: 'stacks' } }"
:exact="true"
@@ -206,7 +194,7 @@
</v-list-item>
<v-list-item
v-show="isSponsor"
v-show="config.count.documents > 0"
:to="{ name: 'browse', query: { q: 'documents' } }"
:exact="true"
variant="text"
@@ -216,6 +204,7 @@
<v-list-item-title :class="`nav-menu-item menu-item`">
{{ $gettext(`Documents`) }}
</v-list-item-title>
<span v-show="config.count.documents > 0" class="nav-count-item">{{ config.count.documents }}</span>
</v-list-item>
<v-list-item
@@ -305,21 +294,21 @@
<v-list-item
v-if="isMini && $config.feature('videos')"
to="/videos"
to="/media"
variant="text"
class="nav-video"
class="nav-media"
:ripple="false"
@click.stop=""
>
<v-icon class="ma-auto">mdi-play-circle</v-icon>
</v-list-item>
<div v-else-if="!isMini && $config.feature('videos')">
<v-list-item to="/videos" variant="text" class="nav-video activator" @click.stop="">
<v-list-item to="/media" variant="text" class="nav-media activator" @click.stop="">
<v-list-item-title class="nav-menu-item">
<p class="nav-item-title">
{{ $gettext(`Videos`) }}
{{ $gettext(`Media`) }}
</p>
<span v-show="config.count.videos > 0" class="nav-count-group">{{ config.count.videos }}</span>
<span v-show="config.count.media > 0" class="nav-count-group">{{ config.count.media }}</span>
</v-list-item-title>
</v-list-item>
@@ -330,12 +319,45 @@
</v-list-item>
</template>
<v-list-item :to="{ name: 'videos' }" variant="text" class="nav-video nav-videos" @click.stop="">
<v-list-item-title :class="`nav-menu-item menu-item`">
{{ $gettext(`Videos`) }}
</v-list-item-title>
<span v-show="config.count.videos > 0" class="nav-count-item">{{ config.count.videos }}</span>
</v-list-item>
<v-list-item :to="{ name: 'live' }" variant="text" class="nav-live" @click.stop="">
<v-list-item-title :class="`nav-menu-item menu-item`">
{{ $gettext(`Live Photos`) }}
</v-list-item-title>
<span v-show="config.count.live > 0" class="nav-count-item">{{ config.count.live }}</span>
</v-list-item>
<v-list-item
v-show="config.count.audio > 0"
:to="{ name: 'audio' }"
variant="text"
class="nav-audio"
@click.stop=""
>
<v-list-item-title :class="`nav-menu-item menu-item`">
{{ $gettext(`Audio`) }}
</v-list-item-title>
<span class="nav-count-item">{{ config.count.audio }}</span>
</v-list-item>
<v-list-item
v-show="config.count.animated > 0"
:to="{ name: 'animated' }"
variant="text"
class="nav-animated"
@click.stop=""
>
<v-list-item-title :class="`nav-menu-item menu-item`">
{{ $gettext(`Animated`) }}
</v-list-item-title>
<span v-show="config.count.animated > 0" class="nav-count-item">{{ config.count.animated }}</span>
</v-list-item>
</v-list-group>
</div>

View File

@@ -241,13 +241,15 @@
}
.pswp__dynamic-caption--on-hor-edge {
padding-top: 1px;
padding-top: 0;
padding-left: 52px;
padding-right: 52px;
}
.pswp__dynamic-caption.pswp__dynamic-caption--on-hor-edge h4 {
font-size: 15px;
margin-top: 0;
padding-top: 0;
font-size: 16px;
}
.pswp__dynamic-caption--mobile {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -398,7 +398,7 @@ export class Photo extends RestModel {
}
generateIsPlayable = memoizeOne((type, files) => {
if (type === media.Animated) {
if (type === media.Animated || type === media.Audio) {
return true;
} else if (!files) {
return false;

View File

@@ -128,6 +128,7 @@ export const StartPages = (features) => [
{ value: "default", text: $gettext("Default"), visible: true },
{ value: "browse", text: $gettext("Search"), props: { disabled: !features?.library } },
{ value: "albums", text: $gettext("Albums"), props: { disabled: !features?.albums } },
{ value: "media", text: $gettext("Media"), props: { disabled: !features?.videos } },
{ value: "videos", text: $gettext("Videos"), props: { disabled: !features?.videos } },
{ value: "people", text: $gettext("People"), props: { disabled: !(features?.people && features?.edit) } },
{ value: "favorites", text: $gettext("Favorites"), props: { disabled: !features?.favorites } },
@@ -204,10 +205,6 @@ export const PhotoTypes = () => [
text: $gettext("Raw"),
value: media.Raw,
},
{
text: $gettext("Animated"),
value: media.Animated,
},
{
text: $gettext("Live"),
value: media.Live,
@@ -216,6 +213,14 @@ export const PhotoTypes = () => [
text: $gettext("Video"),
value: media.Video,
},
{
text: $gettext("Audio"),
value: media.Audio,
},
{
text: $gettext("Animated"),
value: media.Animated,
},
{
text: $gettext("Vector"),
value: media.Vector,

View File

@@ -71,7 +71,11 @@ func registerCountMetrics(factory promauto.Factory, counts config.ClientCounts)
metric.With(prometheus.Labels{"stat": "all"}).Set(float64(counts.All))
metric.With(prometheus.Labels{"stat": "photos"}).Set(float64(counts.Photos))
metric.With(prometheus.Labels{"stat": "media"}).Set(float64(counts.Media))
metric.With(prometheus.Labels{"stat": "live"}).Set(float64(counts.Live))
metric.With(prometheus.Labels{"stat": "videos"}).Set(float64(counts.Videos))
metric.With(prometheus.Labels{"stat": "audio"}).Set(float64(counts.Audio))
metric.With(prometheus.Labels{"stat": "documents"}).Set(float64(counts.Documents))
metric.With(prometheus.Labels{"stat": "albums"}).Set(float64(counts.Albums))
metric.With(prometheus.Labels{"stat": "folders"}).Set(float64(counts.Folders))
metric.With(prometheus.Labels{"stat": "files"}).Set(float64(counts.Files))

View File

@@ -144,8 +144,12 @@ type ClientDisable struct {
type ClientCounts struct {
All int `json:"all"`
Photos int `json:"photos"`
Media int `json:"media"`
Animated int `json:"animated"`
Live int `json:"live"`
Audio int `json:"audio"`
Videos int `json:"videos"`
Documents int `json:"documents"`
Cameras int `json:"cameras"`
Lenses int `json:"lenses"`
Countries int `json:"countries"`
@@ -551,10 +555,13 @@ func (c *Config) ClientUser(withSettings bool) *ClientConfig {
if hidePrivate {
c.Db().
Table("photos").
Select("SUM(photo_type = 'video' AND photo_quality > -1 AND photo_private = 0) AS videos, " +
Select("SUM(photo_type = 'animated' AND photo_quality > -1 AND photo_private = 0) AS animated, " +
"SUM(photo_type = 'video' AND photo_quality > -1 AND photo_private = 0) AS videos, " +
"SUM(photo_type = 'live' AND photo_quality > -1 AND photo_private = 0) AS live, " +
"SUM(photo_type = 'audio' AND photo_quality > -1 AND photo_private = 0) AS audio, " +
"SUM(photo_type = 'document' AND photo_quality > -1 AND photo_private = 0) AS documents, " +
"SUM(photo_quality = -1) AS hidden, " +
"SUM(photo_type NOT IN ('live', 'video') AND photo_quality > -1 AND photo_private = 0) AS photos, " +
"SUM(photo_type NOT IN ('animated','video','live','audio','document') AND photo_quality > -1 AND photo_private = 0) AS photos, " +
"SUM(photo_quality BETWEEN 0 AND 2) AS review, " +
"SUM(photo_favorite = 1 AND photo_private = 0 AND photo_quality > -1) AS favorites, " +
"SUM(photo_private = 1 AND photo_quality > -1) AS private").
@@ -564,10 +571,13 @@ func (c *Config) ClientUser(withSettings bool) *ClientConfig {
} else {
c.Db().
Table("photos").
Select("SUM(photo_type = 'video' AND photo_quality > -1) AS videos, " +
Select("SUM(photo_type = 'animated' AND photo_quality > -1) AS animated, " +
"SUM(photo_type = 'video' AND photo_quality > -1) AS videos, " +
"SUM(photo_type = 'live' AND photo_quality > -1) AS live, " +
"SUM(photo_type = 'audio' AND photo_quality > -1) AS audio, " +
"SUM(photo_type = 'document' AND photo_quality > -1) AS documents, " +
"SUM(photo_quality = -1) AS hidden, " +
"SUM(photo_type NOT IN ('live', 'video') AND photo_quality > -1) AS photos, " +
"SUM(photo_type NOT IN ('animated','video','live','audio','document') AND photo_quality > -1) AS photos, " +
"SUM(photo_quality BETWEEN 0 AND 2) AS review, " +
"SUM(photo_favorite = 1 AND photo_quality > -1) AS favorites, " +
"0 AS private").
@@ -585,8 +595,9 @@ func (c *Config) ClientUser(withSettings bool) *ClientConfig {
Take(&cfg.Count)
}
// Calculate total count.
cfg.Count.All = cfg.Count.Photos + cfg.Count.Live + cfg.Count.Videos
// Calculate total counts.
cfg.Count.Media = cfg.Count.Animated + cfg.Count.Live + cfg.Count.Videos + cfg.Count.Audio
cfg.Count.All = cfg.Count.Photos + cfg.Count.Media + cfg.Count.Documents
// Exclude pictures in review from total count.
if c.Settings().Features.Review {

View File

@@ -322,6 +322,12 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
case terms["video"]:
frm.Query = strings.ReplaceAll(frm.Query, "video", "")
frm.Video = true
case terms["audio"]:
frm.Query = strings.ReplaceAll(frm.Query, "audio", "")
frm.Audio = true
case terms["sounds"]:
frm.Query = strings.ReplaceAll(frm.Query, "sounds", "")
frm.Audio = true
case terms["documents"]:
frm.Query = strings.ReplaceAll(frm.Query, "documents", "")
frm.Document = true
@@ -629,24 +635,26 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
// Filter by media type.
if txt.NotEmpty(frm.Type) {
s = s.Where("photos.photo_type IN (?)", SplitOr(strings.ToLower(frm.Type)))
} else if frm.Animated {
s = s.Where("photos.photo_type = ?", media.Animated)
} else if frm.Audio {
s = s.Where("photos.photo_type = ?", media.Audio)
} else if frm.Document {
s = s.Where("photos.photo_type = ?", media.Document)
} else if frm.Image {
s = s.Where("photos.photo_type = ?", media.Image)
} else if frm.Live {
s = s.Where("photos.photo_type = ?", media.Live)
} else if frm.Raw {
s = s.Where("photos.photo_type = ?", media.Raw)
} else if frm.Vector {
s = s.Where("photos.photo_type = ?", media.Vector)
} else if frm.Animated {
s = s.Where("photos.photo_type = ?", media.Animated)
} else if frm.Audio {
s = s.Where("photos.photo_type = ?", media.Audio)
} else if frm.Video {
s = s.Where("photos.photo_type = ?", media.Video)
} else if frm.Live {
s = s.Where("photos.photo_type = ?", media.Live)
} else if frm.Media {
s = s.Where("photos.photo_type IN ('live','video','audio','animated')")
} else if frm.Photo {
s = s.Where("photos.photo_type IN ('image','raw','live','animated','vector')")
s = s.Where("photos.photo_type IN ('image','raw','live')")
}
// Filter by storage path.

View File

@@ -250,6 +250,12 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
case terms["video"]:
frm.Query = strings.ReplaceAll(frm.Query, "video", "")
frm.Video = true
case terms["audio"]:
frm.Query = strings.ReplaceAll(frm.Query, "audio", "")
frm.Audio = true
case terms["sounds"]:
frm.Query = strings.ReplaceAll(frm.Query, "sounds", "")
frm.Audio = true
case terms["documents"]:
frm.Query = strings.ReplaceAll(frm.Query, "documents", "")
frm.Document = true
@@ -524,24 +530,26 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
// Filter by media type.
if txt.NotEmpty(frm.Type) {
s = s.Where("photos.photo_type IN (?)", SplitOr(strings.ToLower(frm.Type)))
} else if frm.Animated {
s = s.Where("photos.photo_type = ?", media.Animated)
} else if frm.Audio {
s = s.Where("photos.photo_type = ?", media.Audio)
} else if frm.Document {
s = s.Where("photos.photo_type = ?", media.Document)
} else if frm.Image {
s = s.Where("photos.photo_type = ?", media.Image)
} else if frm.Live {
s = s.Where("photos.photo_type = ?", media.Live)
} else if frm.Raw {
s = s.Where("photos.photo_type = ?", media.Raw)
} else if frm.Vector {
s = s.Where("photos.photo_type = ?", media.Vector)
} else if frm.Animated {
s = s.Where("photos.photo_type = ?", media.Animated)
} else if frm.Audio {
s = s.Where("photos.photo_type = ?", media.Audio)
} else if frm.Video {
s = s.Where("photos.photo_type = ?", media.Video)
} else if frm.Live {
s = s.Where("photos.photo_type = ?", media.Live)
} else if frm.Media {
s = s.Where("photos.photo_type IN ('live','video','audio','animated')")
} else if frm.Photo {
s = s.Where("photos.photo_type IN ('image','raw','live','animated','vector')")
s = s.Where("photos.photo_type IN ('image','raw','live')")
}
// Filter by storage path.

View File

@@ -45,7 +45,7 @@ func (m GeoResult) Lng() float64 {
// IsPlayable returns true if the photo has a related video/animation that is playable.
func (m GeoResult) IsPlayable() bool {
switch m.PhotoType {
case entity.MediaVideo, entity.MediaLive, entity.MediaAnimated:
case entity.MediaLive, entity.MediaVideo, entity.MediaAudio, entity.MediaAnimated:
return true
default:
return false

View File

@@ -189,7 +189,7 @@ func (m *Photo) Restore() error {
// IsPlayable returns true if the photo has a related video/animation that is playable.
func (m *Photo) IsPlayable() bool {
switch m.PhotoType {
case entity.MediaVideo, entity.MediaLive, entity.MediaAnimated:
case entity.MediaLive, entity.MediaVideo, entity.MediaAudio, entity.MediaAnimated:
return true
default:
return false

View File

@@ -25,15 +25,16 @@ type SearchPhotos struct {
Stack bool `form:"stack" notes:"Finds pictures with more than one media file"`
Unstacked bool `form:"unstacked" notes:"Finds pictures with a file that has been removed from a stack"`
Stackable bool `form:"stackable" notes:"Finds pictures that can be stacked with additional media files"`
Animated bool `form:"animated" notes:"Finds animations only"`
Audio bool `form:"audio" notes:"Finds audio recordings only"`
Document bool `form:"document" notes:"Finds documents only"`
Image bool `form:"image" notes:"Finds regular images only"`
Raw bool `form:"raw" notes:"Finds RAW images only"`
Photo bool `form:"photo" notes:"Finds regular photos and images, as well as RAW and Live Photos"`
Image bool `form:"image" notes:"Finds regular photos and images only"`
Raw bool `form:"raw" notes:"Finds RAW photos only"`
Media bool `form:"media" notes:"Finds Live Photos, videos, audio and animated content"`
Animated bool `form:"animated" notes:"Finds animated content only"`
Audio bool `form:"audio" notes:"Finds audio content only"`
Video bool `form:"video" notes:"Finds videos not categorized as Live Photos"`
Live bool `form:"live" notes:"Finds Live Photos and short videos"`
Vector bool `form:"vector" notes:"Finds vector graphics only"`
Video bool `form:"video" notes:"Finds videos only"`
Photo bool `form:"photo" notes:"Excludes videos and documents from search results"`
Document bool `form:"document" notes:"Finds PDF documents only"`
Scan string `form:"scan" example:"scan:true scan:false" notes:"Finds scanned photos and documents"`
Mp string `form:"mp" example:"mp:3-6" notes:"Resolution in Megapixels (MP)"`
Panorama bool `form:"panorama" notes:"Finds pictures with an aspect ratio > 1.9:1"`

View File

@@ -26,15 +26,16 @@ type SearchPhotosGeo struct {
After time.Time `form:"after" time_format:"2006-01-02" notes:"Finds pictures taken on or after this date"`
Favorite string `form:"favorite" example:"favorite:yes" notes:"Finds favorites only"`
Unsorted bool `form:"unsorted"`
Animated bool `form:"animated" notes:"Finds animations only"`
Audio bool `form:"audio" notes:"Finds audio recordings only"`
Document bool `form:"document" notes:"Finds documents only"`
Image bool `form:"image" notes:"Finds regular images only"`
Raw bool `form:"raw" notes:"Finds RAW images only"`
Photo bool `form:"photo" notes:"Finds regular photos and images, as well as RAW and Live Photos"`
Image bool `form:"image" notes:"Finds regular photos and images only"`
Raw bool `form:"raw" notes:"Finds RAW photos only"`
Media bool `form:"media" notes:"Finds Live Photos, videos, audio and animated content"`
Animated bool `form:"animated" notes:"Finds animated content only"`
Audio bool `form:"audio" notes:"Finds audio content only"`
Video bool `form:"video" notes:"Finds videos not categorized as Live Photos"`
Live bool `form:"live" notes:"Finds Live Photos and short videos"`
Vector bool `form:"vector" notes:"Finds vector graphics only"`
Video bool `form:"video" notes:"Finds videos only"`
Photo bool `form:"photo" notes:"Excludes videos and documents from search results"`
Document bool `form:"document" notes:"Finds PDF documents only"`
Scan string `form:"scan" example:"scan:true scan:false" notes:"Finds scanned photos and documents"`
Mp string `form:"mp" example:"mp:3-6" notes:"Resolution in Megapixels (MP)"`
Panorama bool `form:"panorama"`

View File

@@ -896,14 +896,26 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
})
}
if photo.PhotoType == entity.MediaVideo {
event.Publish("count.videos", event.Data{
if photo.PhotoType == entity.MediaAnimated {
event.Publish("count.animated", event.Data{
"count": 1,
})
} else if photo.PhotoType == entity.MediaLive {
event.Publish("count.live", event.Data{
"count": 1,
})
} else if photo.PhotoType == entity.MediaAudio {
event.Publish("count.audio", event.Data{
"count": 1,
})
} else if photo.PhotoType == entity.MediaVideo {
event.Publish("count.videos", event.Data{
"count": 1,
})
} else if photo.PhotoType == entity.MediaDocument {
event.Publish("count.documents", event.Data{
"count": 1,
})
} else {
event.Publish("count.photos", event.Data{
"count": 1,