Browse Source

improve the index page

master
Nicolas Massé 6 years ago
parent
commit
7dbbffbf41
  1. 17
      main.go
  2. 65
      media_store.go
  3. 36
      web.go
  4. 97
      web/css/main.css
  5. 31
      web/index.html.template

17
main.go

@ -61,6 +61,11 @@ func initConfig() {
viper.SetDefault("Telegram.TokenGenerator.GlobalValidity", 7) viper.SetDefault("Telegram.TokenGenerator.GlobalValidity", 7)
viper.SetDefault("Telegram.TokenGenerator.PerAlbumValidity", 15) viper.SetDefault("Telegram.TokenGenerator.PerAlbumValidity", 15)
// Web Interface I18n
viper.SetDefault("WebInterface.I18n.AllAlbums", "All my albums")
viper.SetDefault("WebInterface.I18n.Bio", "Hello, I'm the photo bot. Here are all the photos and videos collected so far.")
viper.SetDefault("WebInterface.I18n.LastMedia", "My last photos and videos")
viper.SetConfigName("photo-bot") // name of config file (without extension) viper.SetConfigName("photo-bot") // name of config file (without extension)
viper.AddConfigPath("/etc/photo-bot/") viper.AddConfigPath("/etc/photo-bot/")
viper.AddConfigPath("$HOME/.photo-bot") viper.AddConfigPath("$HOME/.photo-bot")
@ -168,6 +173,7 @@ func getMessagesFromConfig() TelegramMessages {
NoUsername: viper.GetString("Telegram.Messages.NoUsername"), NoUsername: viper.GetString("Telegram.Messages.NoUsername"),
SharedAlbum: viper.GetString("Telegram.Messages.SharedAlbum"), SharedAlbum: viper.GetString("Telegram.Messages.SharedAlbum"),
SharedGlobal: viper.GetString("Telegram.Messages.SharedGlobal"), SharedGlobal: viper.GetString("Telegram.Messages.SharedGlobal"),
ThankYouMedia: viper.GetString("Telegram.Messages.ThankYouMedia"),
} }
} }
@ -184,6 +190,15 @@ func getSecretKey(configKey string, minLength int) []byte {
return key return key
} }
func getWebI18nFromConfig() I18n {
var i18n I18n
i18n.SiteName = viper.GetString("WebInterface.SiteName")
i18n.AllAlbums = viper.GetString("WebInterface.I18n.AllAlbums")
i18n.Bio = viper.GetString("WebInterface.I18n.Bio")
i18n.LastMedia = viper.GetString("WebInterface.I18n.LastMedia")
return i18n
}
func main() { func main() {
initConfig() initConfig()
validateConfig() validateConfig()
@ -248,7 +263,7 @@ func main() {
panic(err) panic(err)
} }
web.MediaStore = mediaStore web.MediaStore = mediaStore
web.SiteName = viper.GetString("WebInterface.SiteName") web.I18n = getWebI18nFromConfig()
// Setup the security frontend // Setup the security frontend
var oidc OpenIdSettings = OpenIdSettings{ var oidc OpenIdSettings = OpenIdSettings{

65
media_store.go

@ -22,10 +22,11 @@ type MediaStore struct {
} }
type Album struct { type Album struct {
ID string `yaml:"-"` // Not part of the YAML struct ID string `yaml:"-"` // Not part of the YAML struct
Title string `yaml:"title"` Title string `yaml:"title"`
Date time.Time `yaml:"date"` Date time.Time `yaml:"date"`
Media []Media `yaml:"-"` // Not part of the YAML struct Media []Media `yaml:"-"` // Not part of the YAML struct
CoverMedia Media `yaml:"cover,omitempty"`
} }
type Media struct { type Media struct {
@ -36,6 +37,11 @@ type Media struct {
Date time.Time `yaml:"date"` Date time.Time `yaml:"date"`
} }
// A media without ID will not be serialized in YAML
func (m *Media) IsZero() bool {
return m.ID == ""
}
func InitMediaStore(storeLocation string) (*MediaStore, error) { func InitMediaStore(storeLocation string) (*MediaStore, error) {
err := os.MkdirAll(filepath.Join(storeLocation, ".current"), os.ModePerm) err := os.MkdirAll(filepath.Join(storeLocation, ".current"), os.ModePerm)
if err != nil { if err != nil {
@ -160,6 +166,17 @@ func (store *MediaStore) GetAlbum(name string, metadataOnly bool) (*Album, error
return nil, err return nil, err
} }
// If there is a cover media defined, find the corresponding files
if !album.CoverMedia.IsZero() {
paths, err := filepath.Glob(filepath.Join(store.StoreLocation, filename, album.CoverMedia.ID+".*"))
if err == nil { // Best effort
album.CoverMedia.Files = make([]string, len(paths))
for j, path := range paths {
album.CoverMedia.Files[j] = filepath.Base(path)
}
}
}
if metadataOnly { if metadataOnly {
return &album, nil return &album, nil
} }
@ -169,6 +186,10 @@ func (store *MediaStore) GetAlbum(name string, metadataOnly bool) (*Album, error
return nil, err return nil, err
} }
if album.CoverMedia.IsZero() {
album.setDefaultCover()
}
return &album, nil return &album, nil
} }
@ -244,19 +265,43 @@ func (store *MediaStore) GetCurrentAlbum() (*Album, error) {
return store.GetAlbum("", true) return store.GetAlbum("", true)
} }
func (album *Album) setDefaultCover() {
if len(album.Media) > 0 {
var cover Media
for _, media := range album.Media {
if media.Type == "photo" { // use the first photo of the album as cover media
cover = media
break
}
if cover.IsZero() { // otherwise, fallback to the first media
cover = media
}
}
album.CoverMedia = cover
}
}
func (store *MediaStore) CloseAlbum() error { func (store *MediaStore) CloseAlbum() error {
yamlData, err := ioutil.ReadFile(filepath.Join(store.StoreLocation, ".current", "meta.yaml")) album, err := store.GetAlbum("", false)
if err != nil { if err != nil {
return err return err
} }
var metadata Album if album.CoverMedia.ID != "" {
err = yaml.UnmarshalStrict(yamlData, &metadata) // Write back the metadata
if err != nil { yamlData, err := yaml.Marshal(album)
return err if err != nil {
return err
}
err = ioutil.WriteFile(filepath.Join(store.StoreLocation, ".current", "meta.yaml"), yamlData, 0644)
if err != nil {
return err
}
} }
folderName := metadata.Date.Format("2006-01-02") + "-" + sanitizeAlbumName(metadata.Title) folderName := album.Date.Format("2006-01-02") + "-" + sanitizeAlbumName(album.Title)
err = os.Rename(filepath.Join(store.StoreLocation, ".current"), filepath.Join(store.StoreLocation, folderName)) err = os.Rename(filepath.Join(store.StoreLocation, ".current"), filepath.Join(store.StoreLocation, folderName))
if err != nil { if err != nil {
return err return err

36
web.go

@ -15,7 +15,14 @@ type WebInterface struct {
AlbumTemplate *template.Template AlbumTemplate *template.Template
MediaTemplate *template.Template MediaTemplate *template.Template
IndexTemplate *template.Template IndexTemplate *template.Template
SiteName string I18n I18n
}
type I18n struct {
SiteName string
Bio string
LastMedia string
AllAlbums string
} }
func NewWebInterface(statikFS http.FileSystem) (*WebInterface, error) { func NewWebInterface(statikFS http.FileSystem) (*WebInterface, error) {
@ -116,6 +123,19 @@ func (web *WebInterface) handleDisplayAlbum(w http.ResponseWriter, r *http.Reque
} }
func (web *WebInterface) handleDisplayIndex(w http.ResponseWriter, r *http.Request) { func (web *WebInterface) handleDisplayIndex(w http.ResponseWriter, r *http.Request) {
lastAlbum, err := web.MediaStore.GetAlbum("", false)
if err != nil {
log.Printf("MediaStore.GetAlbum(latest): %s", err)
web.handleError(w, r)
return
}
mediaCount := len(lastAlbum.Media)
if mediaCount >= 5 { // Max 5 media
mediaCount = 5
}
lastMedia := lastAlbum.Media[len(lastAlbum.Media)-mediaCount : len(lastAlbum.Media)]
albums, err := web.MediaStore.ListAlbums() albums, err := web.MediaStore.ListAlbums()
if err != nil { if err != nil {
log.Printf("MediaStore.ListAlbums: %s", err) log.Printf("MediaStore.ListAlbums: %s", err)
@ -124,11 +144,19 @@ func (web *WebInterface) handleDisplayIndex(w http.ResponseWriter, r *http.Reque
} }
sort.Sort(sort.Reverse(albums)) sort.Sort(sort.Reverse(albums))
if len(albums) > 0 && albums[0].ID == "" {
// Latest album should be the first item. Replace it with the one retrieved above
// with metadata loaded.
albums[0] = *lastAlbum
}
err = web.IndexTemplate.Execute(w, struct { err = web.IndexTemplate.Execute(w, struct {
Title string I18n I18n
Albums []Album LastMedia []Media
Albums []Album
}{ }{
web.SiteName, web.I18n,
lastMedia,
albums, albums,
}) })
if err != nil { if err != nil {

97
web/css/main.css

@ -2,22 +2,34 @@
/* Common */ /* Common */
h1 { body {
font-family: sans-serif; font-family: sans-serif;
} }
a {
color: inherit; /* blue colors for links too */
text-decoration: inherit; /* no underline */
}
html { html {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
li img, li video {
max-height: 100%;
min-width: 100%;
object-fit: cover;
vertical-align: bottom;
}
/* Album */ /* Album */
body.album { body.album {
margin: 3vh; margin: 3vh;
} }
.album ul { body.album ul {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: flex-start; align-items: flex-start;
@ -25,7 +37,7 @@ body.album {
margin: 0; margin: 0;
} }
.album li { body.album li {
/* /*
* Line height is expressed as a factor of the viewport width since * Line height is expressed as a factor of the viewport width since
* we want to fit roughly the same amount of photos per line, * we want to fit roughly the same amount of photos per line,
@ -37,14 +49,7 @@ body.album {
padding: 1px; padding: 1px;
} }
.album li img, .album li video { body.album li:last-child {
max-height: 100%;
min-width: 100%;
object-fit: cover;
vertical-align: bottom;
}
.album li:last-child {
flex-grow: 10; flex-grow: 10;
} }
@ -66,11 +71,77 @@ body.media h1 {
margin-right: 3vh; margin-right: 3vh;
} }
.media img, .media video { body.media img, body.media video {
object-fit: contain; object-fit: contain;
max-height: 80%; max-height: 80%;
max-width: 100%;
margin-top: 3vh; margin-top: 3vh;
margin-left: 3vh; margin-left: 3vh;
margin-right: 3vh; margin-right: 3vh;
}
/* Index */
body.index {
margin: 3vw;
}
body.index h2 {
margin-top: 2em;
}
body.index h1 {
text-align: center;
}
body.index ul.media {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
padding: 0;
margin: 0;
}
body.index ul.media li {
/*
* Line height is expressed as a factor of the viewport width since
* we want to fit roughly the same amount of photos per line,
* independently of the user's device
*/
height: 13vw;
flex-grow: 0.5;
list-style-type: none;
padding-right: 1vw;
}
body.index ul.media li:last-child {
flex-grow: 10;
}
body.index ul.album {
display: grid;
grid-gap: 3vw;
grid-auto-flow: row;
grid-template-columns: 15vw 15vw 15vw 15vw 15vw;
/*
* Line height is expressed as a factor of the viewport width since
* we want to fit roughly the same amount of photos per line,
* independently of the user's device
*/
grid-auto-rows: 13vw;
padding: 0;
margin: 0;
}
body.index ul.album div {
margin-top: 1vh;
font-size: large;
}
body.index ul.album li {
list-style-type: none;
text-align: center;
}
body.index .no-cover {
font-size: 4em;
} }

31
web/index.html.template

@ -1,21 +1,42 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>{{ .Title }}</title> <title>{{ .I18n.SiteName }}</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=contain"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=contain">
<link rel="stylesheet" href="/css/main.css"> <link rel="stylesheet" href="/css/main.css">
</head> </head>
<body class="index"> <body class="index">
<h1>{{ .Title }}</h1> <h1>{{ .I18n.SiteName }}</h1>
<ul> <p>{{ .I18n.Bio }}</p>
<h2>{{ .I18n.LastMedia }}</h2>
<ul class="media">
{{ range .LastMedia }}
<li>
<a href="latest/media/{{ .ID }}/"><img src="latest/raw/{{ .Files|photo }}" /></a>
</li>
{{ end }}
<li><!-- Empty Flex element so that "justify-content: space-between" work as expected --></li>
</ul>
<h2>{{ .I18n.AllAlbums }}</h2>
<ul class="album">
{{ range .Albums }} {{ range .Albums }}
<li> <li>
{{ if eq .ID "" }} {{ if eq .ID "" }}
<a href="latest/">{{ .Date|short }} {{ .Title }}</a> <a href="latest/">
{{ else }}
<a href="{{ .ID }}/">
{{ end }}
{{ if ne .CoverMedia.ID "" }}
{{ if eq .ID "" }}
<img src="latest/raw/{{ .CoverMedia.Files|photo }}" />
{{ else }}
<img src="{{ .ID }}/raw/{{ .CoverMedia.Files|photo }}" />
{{ end }}
{{ else }} {{ else }}
<a href="{{ .ID }}/">{{ .Date|short }} {{ .Title }}</a> <span class="no-cover">🚧</span>
{{ end }} {{ end }}
<div>{{ .Title }}</div></a>
</li> </li>
{{ end }} {{ end }}
</ul> </ul>

Loading…
Cancel
Save