diff --git a/main.go b/main.go index 5511146..298aa1b 100644 --- a/main.go +++ b/main.go @@ -61,6 +61,11 @@ func initConfig() { viper.SetDefault("Telegram.TokenGenerator.GlobalValidity", 7) 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.AddConfigPath("/etc/photo-bot/") viper.AddConfigPath("$HOME/.photo-bot") @@ -168,6 +173,7 @@ func getMessagesFromConfig() TelegramMessages { NoUsername: viper.GetString("Telegram.Messages.NoUsername"), SharedAlbum: viper.GetString("Telegram.Messages.SharedAlbum"), 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 } +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() { initConfig() validateConfig() @@ -248,7 +263,7 @@ func main() { panic(err) } web.MediaStore = mediaStore - web.SiteName = viper.GetString("WebInterface.SiteName") + web.I18n = getWebI18nFromConfig() // Setup the security frontend var oidc OpenIdSettings = OpenIdSettings{ diff --git a/media_store.go b/media_store.go index 2f31a9f..c3b56fd 100644 --- a/media_store.go +++ b/media_store.go @@ -22,10 +22,11 @@ type MediaStore struct { } type Album struct { - ID string `yaml:"-"` // Not part of the YAML struct - Title string `yaml:"title"` - Date time.Time `yaml:"date"` - Media []Media `yaml:"-"` // Not part of the YAML struct + ID string `yaml:"-"` // Not part of the YAML struct + Title string `yaml:"title"` + Date time.Time `yaml:"date"` + Media []Media `yaml:"-"` // Not part of the YAML struct + CoverMedia Media `yaml:"cover,omitempty"` } type Media struct { @@ -36,6 +37,11 @@ type Media struct { 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) { err := os.MkdirAll(filepath.Join(storeLocation, ".current"), os.ModePerm) if err != nil { @@ -160,6 +166,17 @@ func (store *MediaStore) GetAlbum(name string, metadataOnly bool) (*Album, error 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 { return &album, nil } @@ -169,6 +186,10 @@ func (store *MediaStore) GetAlbum(name string, metadataOnly bool) (*Album, error return nil, err } + if album.CoverMedia.IsZero() { + album.setDefaultCover() + } + return &album, nil } @@ -244,19 +265,43 @@ func (store *MediaStore) GetCurrentAlbum() (*Album, error) { 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 { - yamlData, err := ioutil.ReadFile(filepath.Join(store.StoreLocation, ".current", "meta.yaml")) + album, err := store.GetAlbum("", false) if err != nil { return err } - var metadata Album - err = yaml.UnmarshalStrict(yamlData, &metadata) - if err != nil { - return err + if album.CoverMedia.ID != "" { + // Write back the metadata + yamlData, err := yaml.Marshal(album) + 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)) if err != nil { return err diff --git a/web.go b/web.go index 6baa669..97f4bc3 100644 --- a/web.go +++ b/web.go @@ -15,7 +15,14 @@ type WebInterface struct { AlbumTemplate *template.Template MediaTemplate *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) { @@ -116,6 +123,19 @@ func (web *WebInterface) handleDisplayAlbum(w http.ResponseWriter, r *http.Reque } 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() if err != nil { 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)) + 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 { - Title string - Albums []Album + I18n I18n + LastMedia []Media + Albums []Album }{ - web.SiteName, + web.I18n, + lastMedia, albums, }) if err != nil { diff --git a/web/css/main.css b/web/css/main.css index b9f840f..7e7ccdd 100644 --- a/web/css/main.css +++ b/web/css/main.css @@ -2,22 +2,34 @@ /* Common */ -h1 { +body { font-family: sans-serif; } +a { + color: inherit; /* blue colors for links too */ + text-decoration: inherit; /* no underline */ +} + html { height: 100%; width: 100%; } +li img, li video { + max-height: 100%; + min-width: 100%; + object-fit: cover; + vertical-align: bottom; +} + /* Album */ body.album { margin: 3vh; } -.album ul { +body.album ul { display: flex; flex-wrap: wrap; align-items: flex-start; @@ -25,7 +37,7 @@ body.album { margin: 0; } -.album li { +body.album 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, @@ -37,14 +49,7 @@ body.album { padding: 1px; } -.album li img, .album li video { - max-height: 100%; - min-width: 100%; - object-fit: cover; - vertical-align: bottom; -} - -.album li:last-child { +body.album li:last-child { flex-grow: 10; } @@ -66,11 +71,77 @@ body.media h1 { margin-right: 3vh; } -.media img, .media video { +body.media img, body.media video { object-fit: contain; max-height: 80%; - max-width: 100%; margin-top: 3vh; margin-left: 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; } \ No newline at end of file diff --git a/web/index.html.template b/web/index.html.template index 7c68e94..67b8bd6 100644 --- a/web/index.html.template +++ b/web/index.html.template @@ -1,21 +1,42 @@ - {{ .Title }} + {{ .I18n.SiteName }} -

{{ .Title }}

-