diff --git a/Makefile b/Makefile
index 743c0fd2e391830b12b52616a9acd68b37ef9aa3..76c271a2565bfb16b476e67c6268b9c395441f5b 100644
--- a/Makefile
+++ b/Makefile
@@ -30,7 +30,7 @@
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 035d00aba84f2c6f54ce952f28c9b28b6b719e9e..daf5356263701e47f844e450c6a9d5d491bc7808 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -109,6 +109,7 @@ view.NewAuthView(userController),
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 d6addbf7d2189814875083776bb38b422ed5aee3..9915c90c7f8a3ba35b115113c08ca8cdde69f29d 100644
--- a/pkg/database/repository/media.go
+++ b/pkg/database/repository/media.go
@@ -35,7 +35,9 @@ GPSLongitude *float64
}
Album struct {
- ID uint
+ ID uint
+ Name string
+ Path string
}
MediaThumbnail struct {
@@ -43,9 +45,10 @@ Path string
}
Pagination struct {
- Page int
- Size int
- Path string
+ Page int
+ Size int
+ AlbumID *uint
+ Path string
}
CreateMedia struct {
@@ -83,8 +86,10 @@ GetThumbnail(context.Context, uint) (*MediaThumbnail, error)
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 59e39eebc37dd9a80b8e8f5e9a2452d827e88740..4b48608a19f623d1b6021171ce0942fd88d10fbb 100644
--- a/pkg/database/sql/media.go
+++ b/pkg/database/sql/media.go
@@ -23,7 +23,7 @@ MediaEXIF struct {
gorm.Model
Width *float64
Height *float64
- MediaID uint
+ MediaID uint `gorm:"not null"`
Media Media
Description *string
Camera *string
@@ -43,8 +43,8 @@ }
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 @@ gorm.Model
ParentID *uint
Parent *MediaAlbum
Name string
- Path string
+ Path string `gorm:"not null; unique"`
}
MediaAlbumFile struct {
@@ -104,7 +104,9 @@ }
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 @@
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 @@ }
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 0000000000000000000000000000000000000000..a96b9bd89530fbb8e557a68e2c4fb2f299784748
--- /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 c7d84ec9e8346627ef2b13fab1bc7e0ec949a2d5..3124119f901e683cddd810c4694584937b5c859f 100644
--- a/pkg/view/media.go
+++ b/pkg/view/media.go
@@ -17,12 +17,14 @@ settingsRepository repository.SettingsRepository
}
)
-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 @@ } else {
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 (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 618a184d157d6fba7abfcf3e64823107ddba0583..04af9bceac2ed534d76640f3682c558ade815986 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 0000000000000000000000000000000000000000..ce8111e926fdfff1f11d2947d5822bb7b1ef09c2
--- /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() %}
+<h1 class="title">{%s p.Name %}</h1>
+<div class="tags are-large">
+{% for _, a := range p.Albums %}
+ <a href="/album/?albumId={%s FromUInttoString(&a.ID) %}" class="tag">{%s a.Name %}</a>
+{% endfor %}
+</div>
+<div class="columns is-multiline">
+{% for _, media := range p.Medias %}
+ <div class="card-image">
+ {% if media.IsVideo() %}
+ <video class="image is-fit" controls muted="true" poster="/media/thumbnail/?path_hash={%s media.PathHash %}" preload="{%s p.PreloadAttr() %}">
+ <source src="/media/image/?path_hash={%s media.PathHash %}" type="{%s media.MIMEType %}">
+ </video>
+ {% else %}
+ <figure class="image is-fit">
+ <img src="/media/thumbnail/?path_hash={%s media.PathHash %}">
+ </figure>
+ {% endif %}
+ </div>
+{% endfor %}
+</div>
+<div class="row">
+ <a href="/media/?page={%d p.Next.Page %}" class="button is-pulled-right">next</a>
+</div>
+{% endfunc %}
+
+{% func (p *AlbumPage) Script() %}
+{% endfunc %}
diff --git a/templates/base.qtpl b/templates/base.qtpl
index 5a7c3b769e7767d77cb6fe18985a98d41d5e39f2..772167d70fbbcb8c1374c2f10243122c33085a25 100644
--- a/templates/base.qtpl
+++ b/templates/base.qtpl
@@ -11,8 +11,7 @@ }
%}
-{% code
- func FromUInttoString(u *uint) string {
+{% code func FromUInttoString(u *uint) string {
if u != nil {
return strconv.FormatUint(uint64(*u), 10)
}
@@ -39,6 +38,9 @@ files
</a>
<a href="/media/" class="navbar-item">
media
+ </a>
+ <a href="/album/" class="navbar-item">
+ album
</a>
<a href="/settings/" class="navbar-item">
settings