From c51fb8cc8b850b4915e083d0dd2c30d79f8b632e Mon Sep 17 00:00:00 2001 From: Gabriel Arakaki Giovanini Date: Sun, 13 Aug 2023 21:36:42 +0200 Subject: feat: Add (yet again) crude album implementation This is a initial UI album implementation. This should cover the most basic album navigation. This is still plenty to do :) --- Makefile | 2 +- cmd/server/main.go | 1 + pkg/database/repository/media.go | 13 +++-- pkg/database/sql/media.go | 46 ++++++++++++++-- pkg/view/album.go | 102 ++++++++++++++++++++++++++++++++++++ pkg/view/media.go | 26 ++++++--- pkg/worker/scanner/album_scanner.go | 1 + templates/album.qtpl | 50 ++++++++++++++++++ templates/base.qtpl | 6 ++- 9 files changed, 227 insertions(+), 20 deletions(-) create mode 100644 pkg/view/album.go create mode 100644 templates/album.qtpl diff --git a/Makefile b/Makefile index 743c0fd..76c271a 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ compress: build compress_into_oblivion: build upx --best --ultra-brute $(OUT) -run: sass +run: sass tmpl $(GO_RUN) $(SERVER) \ --db-type=$(DB_TYPE) \ --db-con="$(DB_CON)" \ diff --git a/cmd/server/main.go b/cmd/server/main.go index 035d00a..daf5356 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -109,6 +109,7 @@ func main() { view.NewFileSystemView(*fileSystemController, settingsRepository), view.NewSettingsView(settingsRepository, userController), view.NewMediaView(mediaRepository, userRepository, settingsRepository), + view.NewAlbumView(mediaRepository, userRepository, settingsRepository), } { v.SetMyselfIn(extRouter) } diff --git a/pkg/database/repository/media.go b/pkg/database/repository/media.go index d6addbf..9915c90 100644 --- a/pkg/database/repository/media.go +++ b/pkg/database/repository/media.go @@ -35,7 +35,9 @@ type ( } Album struct { - ID uint + ID uint + Name string + Path string } MediaThumbnail struct { @@ -43,9 +45,10 @@ type ( } Pagination struct { - Page int - Size int - Path string + Page int + Size int + AlbumID *uint + Path string } CreateMedia struct { @@ -83,8 +86,10 @@ type ( CreateThumbnail(context.Context, uint, *MediaThumbnail) error ListEmptyAlbums(context.Context, *Pagination) ([]*Media, error) + ListAlbums(context.Context, uint) ([]*Album, error) ExistsAlbumByAbsolutePath(context.Context, string) (bool, error) GetAlbumByAbsolutePath(context.Context, string) (*Album, error) + GetAlbum(context.Context, uint) (*Album, error) CreateAlbum(context.Context, *CreateAlbum) (*Album, error) CreateAlbumFile(context.Context, *CreateAlbumFile) error } diff --git a/pkg/database/sql/media.go b/pkg/database/sql/media.go index 59e39ee..4b48608 100644 --- a/pkg/database/sql/media.go +++ b/pkg/database/sql/media.go @@ -23,7 +23,7 @@ type ( gorm.Model Width *float64 Height *float64 - MediaID uint + MediaID uint `gorm:"not null"` Media Media Description *string Camera *string @@ -43,8 +43,8 @@ type ( MediaThumbnail struct { gorm.Model - Path string - MediaID uint + Path string `gorm:"not null;unique"` + MediaID uint `gorm:"not null"` Media Media } @@ -53,7 +53,7 @@ type ( ParentID *uint Parent *MediaAlbum Name string - Path string + Path string `gorm:"not null; unique"` } MediaAlbumFile struct { @@ -104,7 +104,9 @@ func (m *MediaEXIF) ToModel() *repository.MediaEXIF { func (a *MediaAlbum) ToModel() *repository.Album { return &repository.Album{ - ID: a.ID, + ID: a.ID, + Name: a.Name, + Path: a.Path, } } @@ -407,6 +409,22 @@ func (r *MediaRepository) GetAlbumByAbsolutePath(ctx context.Context, path strin return m.ToModel(), nil } +func (r *MediaRepository) GetAlbum(ctx context.Context, albumID uint) (*repository.Album, error) { + m := &MediaAlbum{} + result := r.db. + WithContext(ctx). + Model(&MediaAlbum{}). + Where("id = ?", albumID). + Limit(1). + Take(m) + + if result.Error != nil { + return nil, result.Error + } + + return m.ToModel(), nil +} + func (m *MediaRepository) CreateAlbum(ctx context.Context, createAlbum *repository.CreateAlbum) (*repository.Album, error) { album := &MediaAlbum{ ParentID: createAlbum.ParentID, @@ -439,3 +457,21 @@ func (m *MediaRepository) CreateAlbumFile(ctx context.Context, createAlbumFile * return nil } + +func (m *MediaRepository) ListAlbums(ctx context.Context, albumID uint) ([]*repository.Album, error) { + albums := make([]*MediaAlbum, 0) + + result := m.db. + WithContext(ctx). + Model(&MediaAlbum{}). + Where("parent_id = ?", albumID). + Find(&albums) + + if result.Error != nil { + return nil, result.Error + } + + return list.Map(albums, func(a *MediaAlbum) *repository.Album { + return a.ToModel() + }), nil +} diff --git a/pkg/view/album.go b/pkg/view/album.go new file mode 100644 index 0000000..a96b9bd --- /dev/null +++ b/pkg/view/album.go @@ -0,0 +1,102 @@ +package view + +import ( + "net/http" + + "git.sr.ht/~gabrielgio/img/pkg/database/repository" + "git.sr.ht/~gabrielgio/img/pkg/ext" + "git.sr.ht/~gabrielgio/img/templates" +) + +type ( + AlbumView struct { + mediaRepository repository.MediaRepository + userRepository repository.UserRepository + settingsRepository repository.SettingsRepository + } +) + +func NewAlbumView( + mediaRepository repository.MediaRepository, + userRepository repository.UserRepository, + settingsRepository repository.SettingsRepository, +) *AlbumView { + return &AlbumView{ + mediaRepository: mediaRepository, + userRepository: userRepository, + settingsRepository: settingsRepository, + } +} + +func (self *AlbumView) Index(w http.ResponseWriter, r *http.Request) error { + p := getPagination(r) + token := ext.GetTokenFromCtx(w, r) + + // TODO: optmize call, GetPathFromUserID may no be necessary + userPath, err := self.userRepository.GetPathFromUserID(r.Context(), token.UserID) + if err != nil { + return err + } + + var albums []*repository.Album + var album *repository.Album + + if p.AlbumID == nil { + // use user path as default value + p.Path = userPath + + album, err = self.mediaRepository.GetAlbumByAbsolutePath(r.Context(), p.Path) + if err != nil { + return err + } + + albums, err = self.mediaRepository.ListAlbums(r.Context(), album.ID) + if err != nil { + return err + } + } else { + album, err = self.mediaRepository.GetAlbum(r.Context(), *p.AlbumID) + if err != nil { + return err + } + + // TODO: User can enter a album out of its bounderies + p.Path = album.Path + + albums, err = self.mediaRepository.ListAlbums(r.Context(), *p.AlbumID) + if err != nil { + return err + } + + } + + medias, err := self.mediaRepository.List(r.Context(), p) + if err != nil { + return err + } + + settings, err := self.settingsRepository.Load(r.Context()) + if err != nil { + return err + } + + page := &templates.AlbumPage{ + Medias: medias, + Albums: albums, + Name: album.Name, + Next: &repository.Pagination{ + Size: p.Size, + Page: p.Page + 1, + }, + Settings: settings, + } + + templates.WritePageTemplate(w, page) + + return nil +} + +func (self *AlbumView) SetMyselfIn(r *ext.Router) { + r.GET("/album/", self.Index) + r.POST("/album/", self.Index) +} diff --git a/pkg/view/media.go b/pkg/view/media.go index c7d84ec..3124119 100644 --- a/pkg/view/media.go +++ b/pkg/view/media.go @@ -17,12 +17,14 @@ type ( } ) -func getPagination(w http.ResponseWriter, r *http.Request) *repository.Pagination { +func getPagination(r *http.Request) *repository.Pagination { var ( - size int - page int - sizeStr = r.FormValue("size") - pageStr = r.FormValue("page") + size int + page int + albumID *uint + sizeStr = r.FormValue("size") + pageStr = r.FormValue("page") + albumIDStr = r.FormValue("albumId") ) if sizeStr == "" { @@ -41,9 +43,17 @@ func getPagination(w http.ResponseWriter, r *http.Request) *repository.Paginatio page = p } + if albumIDStr == "" { + page = 0 + } else if p, err := strconv.Atoi(albumIDStr); err == nil { + id := uint(p) + albumID = &id + } + return &repository.Pagination{ - Page: page, - Size: size, + Page: page, + Size: size, + AlbumID: albumID, } } @@ -60,7 +70,7 @@ func NewMediaView( } func (self *MediaView) Index(w http.ResponseWriter, r *http.Request) error { - p := getPagination(w, r) + p := getPagination(r) token := ext.GetTokenFromCtx(w, r) userPath, err := self.userRepository.GetPathFromUserID(r.Context(), token.UserID) diff --git a/pkg/worker/scanner/album_scanner.go b/pkg/worker/scanner/album_scanner.go index 618a184..04af9bc 100644 --- a/pkg/worker/scanner/album_scanner.go +++ b/pkg/worker/scanner/album_scanner.go @@ -92,6 +92,7 @@ func FanInwards(paths []string) []string { result := make([]string, 0, len(paths)) for i := (len(paths) - 1); i >= 0; i-- { subPaths := paths[0:i] + subPaths = append([]string{"/"}, subPaths...) result = append(result, path.Join(subPaths...)) } return result diff --git a/templates/album.qtpl b/templates/album.qtpl new file mode 100644 index 0000000..ce8111e --- /dev/null +++ b/templates/album.qtpl @@ -0,0 +1,50 @@ +{% import "git.sr.ht/~gabrielgio/img/pkg/database/repository" %} + +{% code +type AlbumPage struct { + Medias []*repository.Media + Next *repository.Pagination + Settings *repository.Settings + Albums []*repository.Album + Name string +} + +func (m *AlbumPage) PreloadAttr() string { + if m.Settings.PreloadVideoMetadata { + return "metadata" + } + return "none" +} +%} + +{% func (p *AlbumPage) Title() %}Media{% endfunc %} + +{% func (p *AlbumPage) Content() %} +

{%s p.Name %}

+
+{% for _, a := range p.Albums %} + {%s a.Name %} +{% endfor %} +
+
+{% for _, media := range p.Medias %} +
+ {% if media.IsVideo() %} + + {% else %} +
+ +
+ {% endif %} +
+{% endfor %} +
+
+ next +
+{% endfunc %} + +{% func (p *AlbumPage) Script() %} +{% endfunc %} diff --git a/templates/base.qtpl b/templates/base.qtpl index 5a7c3b7..772167d 100644 --- a/templates/base.qtpl +++ b/templates/base.qtpl @@ -11,8 +11,7 @@ Page { %} -{% code - func FromUInttoString(u *uint) string { +{% code func FromUInttoString(u *uint) string { if u != nil { return strconv.FormatUint(uint64(*u), 10) } @@ -40,6 +39,9 @@ Page prints a page implementing Page interface. media + + album + settings -- cgit v1.2.3