mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 21:19:18 +00:00
@@ -55,6 +55,47 @@ func AddMediaShare(db *gorm.DB, userID int, mediaID int, expire *time.Time, pass
|
|||||||
return &shareToken, nil
|
return &shareToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AddAlbumShare(db *gorm.DB, user *models.User, albumID int, expire *time.Time, password *string) (*models.ShareToken, error) {
|
||||||
|
var count int64
|
||||||
|
err := db.
|
||||||
|
Model(&models.Album{}).
|
||||||
|
Where("EXISTS (SELECT * FROM user_albums WHERE user_albums.album_id = albums.id AND user_albums.user_id = ?)", user.ID).
|
||||||
|
Count(&count).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to validate album owner with database")
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return nil, auth.ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
var hashedPassword *string = nil
|
||||||
|
if password != nil {
|
||||||
|
hashedPassBytes, err := bcrypt.GenerateFromPassword([]byte(*password), 12)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to hash token password")
|
||||||
|
}
|
||||||
|
hashedStr := string(hashedPassBytes)
|
||||||
|
hashedPassword = &hashedStr
|
||||||
|
}
|
||||||
|
|
||||||
|
shareToken := models.ShareToken{
|
||||||
|
Value: utils.GenerateToken(),
|
||||||
|
OwnerID: user.ID,
|
||||||
|
Expire: expire,
|
||||||
|
Password: hashedPassword,
|
||||||
|
AlbumID: &albumID,
|
||||||
|
MediaID: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&shareToken).Error; err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to insert new share token into database")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &shareToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
func DeleteShareToken(db *gorm.DB, userID int, tokenValue string) (*models.ShareToken, error) {
|
func DeleteShareToken(db *gorm.DB, userID int, tokenValue string) (*models.ShareToken, error) {
|
||||||
token, err := getUserToken(db, userID, tokenValue)
|
token, err := getUserToken(db, userID, tokenValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func (p *MediaURL) CachedPath() (string, error) {
|
|||||||
return "", errors.New("mediaURL.Media is nil")
|
return "", errors.New("mediaURL.Media is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Purpose == PhotoThumbnail || p.Purpose == PhotoHighRes || p.Purpose == VideoThumbnail {
|
if p.Purpose == PhotoThumbnail || p.Purpose == PhotoHighRes || p.Purpose == VideoThumbnail || p.Purpose == VideoWeb {
|
||||||
cachedPath = path.Join(utils.MediaCachePath(), strconv.Itoa(int(p.Media.AlbumID)), strconv.Itoa(int(p.MediaID)), p.MediaName)
|
cachedPath = path.Join(utils.MediaCachePath(), strconv.Itoa(int(p.Media.AlbumID)), strconv.Itoa(int(p.MediaID)), p.MediaName)
|
||||||
} else if p.Purpose == MediaOriginal {
|
} else if p.Purpose == MediaOriginal {
|
||||||
cachedPath = p.Media.Path
|
cachedPath = p.Media.Path
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/photoview/photoview/api/graphql/auth"
|
"github.com/photoview/photoview/api/graphql/auth"
|
||||||
"github.com/photoview/photoview/api/graphql/models"
|
"github.com/photoview/photoview/api/graphql/models"
|
||||||
"github.com/photoview/photoview/api/graphql/models/actions"
|
"github.com/photoview/photoview/api/graphql/models/actions"
|
||||||
"github.com/photoview/photoview/api/utils"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -100,44 +99,7 @@ func (r *mutationResolver) ShareAlbum(ctx context.Context, albumID int, expire *
|
|||||||
return nil, auth.ErrUnauthorized
|
return nil, auth.ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
var count int64
|
return actions.AddAlbumShare(r.Database, user, albumID, expire, password)
|
||||||
err := r.Database.
|
|
||||||
Model(&models.Album{}).
|
|
||||||
Where("EXISTS (SELECT * FROM user_albums WHERE user_albums.album_id = albums.id AND user_albums.user_id = ?)", user.ID).
|
|
||||||
Count(&count).Error
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to validate album owner with database")
|
|
||||||
}
|
|
||||||
|
|
||||||
if count == 0 {
|
|
||||||
return nil, auth.ErrUnauthorized
|
|
||||||
}
|
|
||||||
|
|
||||||
var hashedPassword *string = nil
|
|
||||||
if password != nil {
|
|
||||||
hashedPassBytes, err := bcrypt.GenerateFromPassword([]byte(*password), 12)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to hash token password")
|
|
||||||
}
|
|
||||||
hashedStr := string(hashedPassBytes)
|
|
||||||
hashedPassword = &hashedStr
|
|
||||||
}
|
|
||||||
|
|
||||||
shareToken := models.ShareToken{
|
|
||||||
Value: utils.GenerateToken(),
|
|
||||||
OwnerID: user.ID,
|
|
||||||
Expire: expire,
|
|
||||||
Password: hashedPassword,
|
|
||||||
AlbumID: &albumID,
|
|
||||||
MediaID: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.Database.Create(&shareToken).Error; err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to insert new share token into database")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &shareToken, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) ShareMedia(ctx context.Context, mediaID int, expire *time.Time, password *string) (*models.ShareToken, error) {
|
func (r *mutationResolver) ShareMedia(ctx context.Context, mediaID int, expire *time.Time, password *string) (*models.ShareToken, error) {
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/photoview/photoview/api/graphql/auth"
|
|
||||||
"github.com/photoview/photoview/api/graphql/models"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func authenticateMedia(media *models.Media, db *gorm.DB, r *http.Request) (success bool, responseMessage string, responseStatus int, errorMessage error) {
|
|
||||||
user := auth.UserFromContext(r.Context())
|
|
||||||
|
|
||||||
if user != nil {
|
|
||||||
var album models.Album
|
|
||||||
if err := db.First(&album, media.AlbumID).Error; err != nil {
|
|
||||||
return false, "internal server error", http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ownsAlbum, err := user.OwnsAlbum(db, &album)
|
|
||||||
if err != nil {
|
|
||||||
return false, "internal server error", http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ownsAlbum {
|
|
||||||
return false, "invalid credentials", http.StatusForbidden, nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Check if photo is authorized with a share token
|
|
||||||
token := r.URL.Query().Get("token")
|
|
||||||
if token == "" {
|
|
||||||
return false, "unauthorized", http.StatusForbidden, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var shareToken models.ShareToken
|
|
||||||
if err := db.Where("value = ?", token).First(&shareToken).Error; err != nil {
|
|
||||||
return false, "internal server error", http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate share token password, if set
|
|
||||||
if shareToken.Password != nil {
|
|
||||||
tokenPasswordCookie, err := r.Cookie(fmt.Sprintf("share-token-pw-%s", shareToken.Value))
|
|
||||||
if err != nil {
|
|
||||||
return false, "unauthorized", http.StatusForbidden, nil
|
|
||||||
}
|
|
||||||
// tokenPassword := r.Header.Get("TokenPassword")
|
|
||||||
tokenPassword := tokenPasswordCookie.Value
|
|
||||||
|
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(*shareToken.Password), []byte(tokenPassword)); err != nil {
|
|
||||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
|
||||||
return false, "unauthorized", http.StatusForbidden, nil
|
|
||||||
} else {
|
|
||||||
return false, "internal server error", http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if shareToken.AlbumID != nil && media.AlbumID != *shareToken.AlbumID {
|
|
||||||
// Check child albums
|
|
||||||
|
|
||||||
var count int
|
|
||||||
err := db.Raw(`
|
|
||||||
WITH recursive child_albums AS (
|
|
||||||
SELECT * FROM albums WHERE parent_album_id = ?
|
|
||||||
UNION ALL
|
|
||||||
SELECT child.* FROM albums child JOIN child_albums parent ON parent.id = child.parent_album_id
|
|
||||||
)
|
|
||||||
SELECT COUNT(id) FROM child_albums WHERE id = ?
|
|
||||||
`, *shareToken.AlbumID, media.AlbumID).Find(&count).Error
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return false, "internal server error", http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if count == 0 {
|
|
||||||
return false, "unauthorized", http.StatusForbidden, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if shareToken.MediaID != nil && media.ID != *shareToken.MediaID {
|
|
||||||
return false, "unauthorized", http.StatusForbidden, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, "success", http.StatusAccepted, nil
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/photoview/photoview/api/graphql/auth"
|
|
||||||
"github.com/photoview/photoview/api/graphql/models"
|
|
||||||
"github.com/photoview/photoview/api/graphql/models/actions"
|
|
||||||
"github.com/photoview/photoview/api/test_utils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAuthenticateMedia(t *testing.T) {
|
|
||||||
db := test_utils.DatabaseTest(t)
|
|
||||||
|
|
||||||
user, err := models.RegisterUser(db, "username", nil, false)
|
|
||||||
if !assert.NoError(t, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
album := models.Album{
|
|
||||||
Title: "my_album",
|
|
||||||
}
|
|
||||||
|
|
||||||
if !assert.NoError(t, db.Model(&user).Association("Albums").Append(&album)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
media := models.Media{
|
|
||||||
Title: "my_media",
|
|
||||||
Path: "/photos/image.jpg",
|
|
||||||
AlbumID: album.ID,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !assert.NoError(t, db.Save(&media).Error) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("Authorized request", func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest("GET", "/photo/image.jpg", strings.NewReader("IMAGE DATA"))
|
|
||||||
ctx := auth.AddUserToContext(req.Context(), user)
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
|
|
||||||
success, responseMessage, responseStatus, err := authenticateMedia(&media, db, req)
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.True(t, success)
|
|
||||||
assert.Equal(t, responseMessage, "success")
|
|
||||||
assert.Equal(t, responseStatus, http.StatusAccepted)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Request without access token", func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest("GET", "/photo/image.jpg", strings.NewReader("IMAGE DATA"))
|
|
||||||
|
|
||||||
success, responseMessage, responseStatus, err := authenticateMedia(&media, db, req)
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.False(t, success)
|
|
||||||
assert.Equal(t, responseMessage, "unauthorized")
|
|
||||||
assert.Equal(t, responseStatus, http.StatusForbidden)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Request without access token", func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest("GET", "/photo/image.jpg", strings.NewReader("IMAGE DATA"))
|
|
||||||
|
|
||||||
success, responseMessage, responseStatus, err := authenticateMedia(&media, db, req)
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.False(t, success)
|
|
||||||
assert.Equal(t, responseMessage, "unauthorized")
|
|
||||||
assert.Equal(t, responseStatus, http.StatusForbidden)
|
|
||||||
})
|
|
||||||
|
|
||||||
expire := time.Now().Add(time.Hour * 24 * 30)
|
|
||||||
tokenPassword := "token-password-123"
|
|
||||||
shareToken, err := actions.AddMediaShare(db, user.ID, media.ID, &expire, &tokenPassword)
|
|
||||||
if !assert.NoError(t, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("Request with share token", func(t *testing.T) {
|
|
||||||
url := fmt.Sprintf("/photo/image.jpg?token=%s", shareToken.Value)
|
|
||||||
req := httptest.NewRequest("GET", url, strings.NewReader("IMAGE DATA"))
|
|
||||||
|
|
||||||
cookie := http.Cookie{
|
|
||||||
Name: fmt.Sprintf("share-token-pw-%s", shareToken.Value),
|
|
||||||
Value: tokenPassword,
|
|
||||||
}
|
|
||||||
req.AddCookie(&cookie)
|
|
||||||
|
|
||||||
success, responseMessage, responseStatus, err := authenticateMedia(&media, db, req)
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.True(t, success)
|
|
||||||
assert.Equal(t, responseMessage, "success")
|
|
||||||
assert.Equal(t, responseStatus, http.StatusAccepted)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
129
api/routes/authenticate_routes.go
Normal file
129
api/routes/authenticate_routes.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/photoview/photoview/api/graphql/auth"
|
||||||
|
"github.com/photoview/photoview/api/graphql/models"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func authenticateMedia(media *models.Media, db *gorm.DB, r *http.Request) (success bool, responseMessage string, responseStatus int, errorMessage error) {
|
||||||
|
user := auth.UserFromContext(r.Context())
|
||||||
|
|
||||||
|
if user != nil {
|
||||||
|
var album models.Album
|
||||||
|
if err := db.First(&album, media.AlbumID).Error; err != nil {
|
||||||
|
return false, "internal server error", http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ownsAlbum, err := user.OwnsAlbum(db, &album)
|
||||||
|
if err != nil {
|
||||||
|
return false, "internal server error", http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ownsAlbum {
|
||||||
|
return false, "invalid credentials", http.StatusForbidden, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if success, respMsg, respStatus, err := shareTokenFromRequest(db, r, &media.ID, &media.AlbumID); !success {
|
||||||
|
return success, respMsg, respStatus, err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, "success", http.StatusAccepted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func authenticateAlbum(album *models.Album, db *gorm.DB, r *http.Request) (success bool, responseMessage string, responseStatus int, errorMessage error) {
|
||||||
|
user := auth.UserFromContext(r.Context())
|
||||||
|
|
||||||
|
if user != nil {
|
||||||
|
ownsAlbum, err := user.OwnsAlbum(db, album)
|
||||||
|
if err != nil {
|
||||||
|
return false, "internal server error", http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ownsAlbum {
|
||||||
|
return false, "invalid credentials", http.StatusForbidden, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if success, respMsg, respStatus, err := shareTokenFromRequest(db, r, nil, &album.ID); !success {
|
||||||
|
return success, respMsg, respStatus, err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, "success", http.StatusAccepted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func shareTokenFromRequest(db *gorm.DB, r *http.Request, mediaID *int, albumID *int) (success bool, responseMessage string, responseStatus int, errorMessage error) {
|
||||||
|
// Check if photo is authorized with a share token
|
||||||
|
token := r.URL.Query().Get("token")
|
||||||
|
if token == "" {
|
||||||
|
return false, "unauthorized", http.StatusForbidden, errors.New("share token not provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
var shareToken models.ShareToken
|
||||||
|
|
||||||
|
if err := db.Where("value = ?", token).First(&shareToken).Error; err != nil {
|
||||||
|
return false, "internal server error", http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate share token password, if set
|
||||||
|
if shareToken.Password != nil {
|
||||||
|
tokenPasswordCookie, err := r.Cookie(fmt.Sprintf("share-token-pw-%s", shareToken.Value))
|
||||||
|
if err != nil {
|
||||||
|
return false, "unauthorized", http.StatusForbidden, errors.Wrap(err, "get share token password cookie")
|
||||||
|
}
|
||||||
|
// tokenPassword := r.Header.Get("TokenPassword")
|
||||||
|
tokenPassword := tokenPasswordCookie.Value
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(*shareToken.Password), []byte(tokenPassword)); err != nil {
|
||||||
|
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||||
|
return false, "unauthorized", http.StatusForbidden, errors.New("incorrect password for share token")
|
||||||
|
} else {
|
||||||
|
return false, "internal server error", http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if shareToken.AlbumID != nil && albumID == nil {
|
||||||
|
return false, "unauthorized", http.StatusForbidden, errors.New("share token is of type album, but no albumID was provided to function")
|
||||||
|
}
|
||||||
|
|
||||||
|
if shareToken.MediaID != nil && mediaID == nil {
|
||||||
|
return false, "unauthorized", http.StatusForbidden, errors.New("share token is of type media, but no mediaID was provided to function")
|
||||||
|
}
|
||||||
|
|
||||||
|
if shareToken.AlbumID != nil && *albumID != *shareToken.AlbumID {
|
||||||
|
// Check child albums
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err := db.Raw(`
|
||||||
|
WITH recursive child_albums AS (
|
||||||
|
SELECT * FROM albums WHERE parent_album_id = ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT child.* FROM albums child JOIN child_albums parent ON parent.id = child.parent_album_id
|
||||||
|
)
|
||||||
|
SELECT COUNT(id) FROM child_albums WHERE id = ?
|
||||||
|
`, *shareToken.AlbumID, albumID).Find(&count).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, "internal server error", http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return false, "unauthorized", http.StatusForbidden, errors.New("no child albums found for share token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if shareToken.MediaID != nil && *mediaID != *shareToken.MediaID {
|
||||||
|
return false, "unauthorized", http.StatusForbidden, errors.New("media share token does not match mediaID")
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, "", 0, nil
|
||||||
|
}
|
||||||
147
api/routes/authenticate_routes_test.go
Normal file
147
api/routes/authenticate_routes_test.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/photoview/photoview/api/graphql/auth"
|
||||||
|
"github.com/photoview/photoview/api/graphql/models"
|
||||||
|
"github.com/photoview/photoview/api/graphql/models/actions"
|
||||||
|
"github.com/photoview/photoview/api/test_utils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAuthenticateRoute(t *testing.T) {
|
||||||
|
db := test_utils.DatabaseTest(t)
|
||||||
|
|
||||||
|
user, err := models.RegisterUser(db, "username", nil, false)
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
album := models.Album{
|
||||||
|
Title: "my_album",
|
||||||
|
Path: "/photos",
|
||||||
|
}
|
||||||
|
|
||||||
|
if !assert.NoError(t, db.Model(&user).Association("Albums").Append(&album)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
media := models.Media{
|
||||||
|
Title: "my_media",
|
||||||
|
Path: "/photos/image.jpg",
|
||||||
|
AlbumID: album.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !assert.NoError(t, db.Save(&media).Error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Authenticate Media", func(t *testing.T) {
|
||||||
|
t.Run("Authorized request", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/photo/image.jpg", strings.NewReader("IMAGE DATA"))
|
||||||
|
ctx := auth.AddUserToContext(req.Context(), user)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
success, responseMessage, responseStatus, err := authenticateMedia(&media, db, req)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, success)
|
||||||
|
assert.Equal(t, responseMessage, "success")
|
||||||
|
assert.Equal(t, responseStatus, http.StatusAccepted)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Request without access token", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/photo/image.jpg", strings.NewReader("IMAGE DATA"))
|
||||||
|
|
||||||
|
success, responseMessage, responseStatus, err := authenticateMedia(&media, db, req)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.False(t, success)
|
||||||
|
assert.Equal(t, responseMessage, "unauthorized")
|
||||||
|
assert.Equal(t, responseStatus, http.StatusForbidden)
|
||||||
|
})
|
||||||
|
|
||||||
|
expire := time.Now().Add(time.Hour * 24 * 30)
|
||||||
|
tokenPassword := "token-password-123"
|
||||||
|
shareToken, err := actions.AddMediaShare(db, user.ID, media.ID, &expire, &tokenPassword)
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Request with share token", func(t *testing.T) {
|
||||||
|
url := fmt.Sprintf("/photo/image.jpg?token=%s", shareToken.Value)
|
||||||
|
req := httptest.NewRequest("GET", url, strings.NewReader("IMAGE DATA"))
|
||||||
|
|
||||||
|
cookie := http.Cookie{
|
||||||
|
Name: fmt.Sprintf("share-token-pw-%s", shareToken.Value),
|
||||||
|
Value: tokenPassword,
|
||||||
|
}
|
||||||
|
req.AddCookie(&cookie)
|
||||||
|
|
||||||
|
success, responseMessage, responseStatus, err := authenticateMedia(&media, db, req)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, success)
|
||||||
|
assert.Equal(t, responseMessage, "success")
|
||||||
|
assert.Equal(t, responseStatus, http.StatusAccepted)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Authenticate Album", func(t *testing.T) {
|
||||||
|
t.Run("Authorized request", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/download/album/1", strings.NewReader("ALBUM DATA"))
|
||||||
|
ctx := auth.AddUserToContext(req.Context(), user)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
success, responseMessage, responseStatus, err := authenticateAlbum(&album, db, req)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, success)
|
||||||
|
assert.Equal(t, responseMessage, "success")
|
||||||
|
assert.Equal(t, responseStatus, http.StatusAccepted)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Request without access token", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/download/album/1", strings.NewReader("ALBUM DATA"))
|
||||||
|
|
||||||
|
success, responseMessage, responseStatus, err := authenticateAlbum(&album, db, req)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.False(t, success)
|
||||||
|
assert.Equal(t, responseMessage, "unauthorized")
|
||||||
|
assert.Equal(t, responseStatus, http.StatusForbidden)
|
||||||
|
})
|
||||||
|
|
||||||
|
expire := time.Now().Add(time.Hour * 24 * 30)
|
||||||
|
tokenPassword := "token-password-123"
|
||||||
|
shareToken, err := actions.AddAlbumShare(db, user, album.ID, &expire, &tokenPassword)
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Request with share token", func(t *testing.T) {
|
||||||
|
url := fmt.Sprintf("/download/album/1?token=%s", shareToken.Value)
|
||||||
|
req := httptest.NewRequest("GET", url, strings.NewReader("ALBUM DATA"))
|
||||||
|
|
||||||
|
cookie := http.Cookie{
|
||||||
|
Name: fmt.Sprintf("share-token-pw-%s", shareToken.Value),
|
||||||
|
Value: tokenPassword,
|
||||||
|
}
|
||||||
|
req.AddCookie(&cookie)
|
||||||
|
|
||||||
|
success, responseMessage, responseStatus, err := authenticateMedia(&media, db, req)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, success)
|
||||||
|
assert.Equal(t, responseMessage, "success")
|
||||||
|
assert.Equal(t, responseStatus, http.StatusAccepted)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
101
api/routes/downloads.go
Normal file
101
api/routes/downloads.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/photoview/photoview/api/graphql/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterDownloadRoutes(db *gorm.DB, router *mux.Router) {
|
||||||
|
router.HandleFunc("/album/{album_id}/{media_purpose}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
albumID := mux.Vars(r)["album_id"]
|
||||||
|
mediaPurpose := mux.Vars(r)["media_purpose"]
|
||||||
|
mediaPurposeList := strings.SplitN(mediaPurpose, ",", 10)
|
||||||
|
|
||||||
|
var album models.Album
|
||||||
|
if err := db.Find(&album, albumID).Error; err != nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
w.Write([]byte("404"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if success, response, status, err := authenticateAlbum(&album, db, r); !success {
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WARN: error authenticating album for download: %v\n", err)
|
||||||
|
}
|
||||||
|
w.WriteHeader(status)
|
||||||
|
w.Write([]byte(response))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var mediaURLs []*models.MediaURL
|
||||||
|
if err := db.Joins("Media").Where("media.album_id = ?", album.ID).Where("media_urls.purpose IN (?)", mediaPurposeList).Find(&mediaURLs).Error; err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte("internal server error"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mediaURLs) == 0 {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Write([]byte("no media found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/zip")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", album.Title))
|
||||||
|
|
||||||
|
zipWriter := zip.NewWriter(w)
|
||||||
|
|
||||||
|
for _, media := range mediaURLs {
|
||||||
|
zipFile, err := zipWriter.Create(fmt.Sprintf("%s/%s", album.Title, media.MediaName))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: Failed to create a file in zip, when downloading album (%d): %v\n", album.ID, err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte("internal server error"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath, err := media.CachedPath()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: Failed to get mediaURL cache path, when downloading album (%d): %v\n", album.ID, err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte("internal server error"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileData, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: Failed to open file to include in zip, when downloading album (%d): %v\n", album.ID, err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte("internal server error"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(zipFile, fileData)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: Failed to copy file data, when downloading album (%d): %v\n", album.ID, err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte("internal server error"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fileData.Close(); err != nil {
|
||||||
|
log.Printf("ERROR: Failed to close file, when downloading album (%d): %v\n", album.ID, err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte("internal server error"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// close the zip Writer to flush the contents to the ResponseWriter
|
||||||
|
zipWriter.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -107,6 +107,9 @@ func main() {
|
|||||||
videoRouter := endpointRouter.PathPrefix("/video").Subrouter()
|
videoRouter := endpointRouter.PathPrefix("/video").Subrouter()
|
||||||
routes.RegisterVideoRoutes(db, videoRouter)
|
routes.RegisterVideoRoutes(db, videoRouter)
|
||||||
|
|
||||||
|
downloadsRouter := endpointRouter.PathPrefix("/download").Subrouter()
|
||||||
|
routes.RegisterDownloadRoutes(db, downloadsRouter)
|
||||||
|
|
||||||
shouldServeUI := utils.ShouldServeUI()
|
shouldServeUI := utils.ShouldServeUI()
|
||||||
|
|
||||||
if shouldServeUI {
|
if shouldServeUI {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import SharePage, {
|
|||||||
VALIDATE_TOKEN_PASSWORD_QUERY,
|
VALIDATE_TOKEN_PASSWORD_QUERY,
|
||||||
} from './SharePage'
|
} from './SharePage'
|
||||||
|
|
||||||
import { SIDEBAR_DOWNLOAD_QUERY } from '../../components/sidebar/SidebarDownload'
|
import { SIDEBAR_DOWNLOAD_QUERY } from '../../components/sidebar/SidebarDownloadMedia'
|
||||||
import { SHARE_ALBUM_QUERY } from './AlbumSharePage'
|
import { SHARE_ALBUM_QUERY } from './AlbumSharePage'
|
||||||
|
|
||||||
jest.mock('../../hooks/useScrollPagination')
|
jest.mock('../../hooks/useScrollPagination')
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ import { MessageState } from './components/messages/Messages'
|
|||||||
import { Message } from './components/messages/SubscriptionsHook'
|
import { Message } from './components/messages/SubscriptionsHook'
|
||||||
import { NotificationType } from './__generated__/globalTypes'
|
import { NotificationType } from './__generated__/globalTypes'
|
||||||
|
|
||||||
export const GRAPHQL_ENDPOINT = process.env.REACT_APP_API_ENDPOINT
|
export const API_ENDPOINT = process.env.REACT_APP_API_ENDPOINT
|
||||||
? urlJoin(process.env.REACT_APP_API_ENDPOINT as string, '/graphql')
|
? (process.env.REACT_APP_API_ENDPOINT as string)
|
||||||
: urlJoin(location.origin, '/api/graphql')
|
: urlJoin(location.origin, '/api')
|
||||||
|
|
||||||
|
export const GRAPHQL_ENDPOINT = urlJoin(API_ENDPOINT, '/graphql')
|
||||||
|
|
||||||
const httpLink = new HttpLink({
|
const httpLink = new HttpLink({
|
||||||
uri: GRAPHQL_ENDPOINT,
|
uri: GRAPHQL_ENDPOINT,
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const SidebarPhotoCover = ({ cover_id }: SidebarPhotoCoverProps) => {
|
|||||||
return (
|
return (
|
||||||
<SidebarSection>
|
<SidebarSection>
|
||||||
<SidebarSectionTitle>
|
<SidebarSectionTitle>
|
||||||
{t('sidebar.album.cover_photo', 'Album cover')}
|
{t('sidebar.album.album_cover', 'Album cover')}
|
||||||
</SidebarSectionTitle>
|
</SidebarSectionTitle>
|
||||||
<div>
|
<div>
|
||||||
<table className="border-collapse w-full">
|
<table className="border-collapse w-full">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
getAlbumSidebarVariables,
|
getAlbumSidebarVariables,
|
||||||
} from './__generated__/getAlbumSidebar'
|
} from './__generated__/getAlbumSidebar'
|
||||||
import { SidebarAlbumCover } from './AlbumCovers'
|
import { SidebarAlbumCover } from './AlbumCovers'
|
||||||
|
import SidebarAlbumDownload from './SidebarDownloadAlbum'
|
||||||
|
|
||||||
const albumQuery = gql`
|
const albumQuery = gql`
|
||||||
query getAlbumSidebar($id: ID!) {
|
query getAlbumSidebar($id: ID!) {
|
||||||
@@ -50,6 +51,9 @@ const AlbumSidebar = ({ albumId }: AlbumSidebarProps) => {
|
|||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<SidebarAlbumCover id={albumId} />
|
<SidebarAlbumCover id={albumId} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-8">
|
||||||
|
<SidebarAlbumDownload albumID={albumId} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
ProtectedVideoProps_Media,
|
ProtectedVideoProps_Media,
|
||||||
} from '../photoGallery/ProtectedMedia'
|
} from '../photoGallery/ProtectedMedia'
|
||||||
import { SidebarPhotoShare } from './Sharing'
|
import { SidebarPhotoShare } from './Sharing'
|
||||||
import SidebarDownload from './SidebarDownload'
|
import SidebarMediaDownload from './SidebarDownloadMedia'
|
||||||
import SidebarItem from './SidebarItem'
|
import SidebarItem from './SidebarItem'
|
||||||
import { SidebarFacesOverlay } from '../facesOverlay/FacesOverlay'
|
import { SidebarFacesOverlay } from '../facesOverlay/FacesOverlay'
|
||||||
import { isNil } from '../../helpers/utils'
|
import { isNil } from '../../helpers/utils'
|
||||||
@@ -364,7 +364,7 @@ const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<MetadataInfo media={media} />
|
<MetadataInfo media={media} />
|
||||||
<SidebarDownload media={media} />
|
<SidebarMediaDownload media={media} />
|
||||||
<SidebarPhotoShare id={media.id} />
|
<SidebarPhotoShare id={media.id} />
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<SidebarPhotoCover cover_id={media.id} />
|
<SidebarPhotoCover cover_id={media.id} />
|
||||||
|
|||||||
85
ui/src/components/sidebar/SidebarDownloadAlbum.tsx
Normal file
85
ui/src/components/sidebar/SidebarDownloadAlbum.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { API_ENDPOINT } from '../../apolloClient'
|
||||||
|
import { SidebarSection, SidebarSectionTitle } from './SidebarComponents'
|
||||||
|
|
||||||
|
type SidebarAlbumDownladProps = {
|
||||||
|
albumID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarAlbumDownload = ({ albumID }: SidebarAlbumDownladProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const downloads = [
|
||||||
|
{
|
||||||
|
title: t('sidebar.album.download.thumbnails.title', 'Thumbnails'),
|
||||||
|
description: t(
|
||||||
|
'sidebar.album.download.thumbnails.description',
|
||||||
|
'Low resolution images, no videos'
|
||||||
|
),
|
||||||
|
purpose: 'thumbnail,video-thumbnail',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t(
|
||||||
|
'sidebar.album.download.high-resolutions.title',
|
||||||
|
'High resolutions'
|
||||||
|
),
|
||||||
|
description: t(
|
||||||
|
'sidebar.album.download.high-resolutions.description',
|
||||||
|
'High resolution jpegs of RAW images'
|
||||||
|
),
|
||||||
|
purpose: 'high-res',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('sidebar.album.download.originals.title', 'Originals'),
|
||||||
|
description: t(
|
||||||
|
'sidebar.album.download.originals.description',
|
||||||
|
'The original images and videos'
|
||||||
|
),
|
||||||
|
purpose: 'original',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('sidebar.album.download.web-videos.title', 'Converted videos'),
|
||||||
|
description: t(
|
||||||
|
'sidebar.album.download.web-videos.description',
|
||||||
|
'Videos that have been optimized for web'
|
||||||
|
),
|
||||||
|
purpose: 'video-web',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const downloadRows = downloads.map(x => (
|
||||||
|
<tr
|
||||||
|
className="cursor-pointer border-gray-100 border-b hover:bg-gray-50 focus:bg-gray-50"
|
||||||
|
key={x.purpose}
|
||||||
|
onClick={() =>
|
||||||
|
(location.href = `${API_ENDPOINT}/download/album/${albumID}/${x.purpose}`)
|
||||||
|
}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<td className="pl-4 py-2">{`${x.title}`}</td>
|
||||||
|
<td className="pr-4 py-2 text-sm text-gray-800 italic">{`${x.description}`}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarSection>
|
||||||
|
<SidebarSectionTitle>
|
||||||
|
{t('sidebar.download.title', 'Download')}
|
||||||
|
</SidebarSectionTitle>
|
||||||
|
|
||||||
|
<table className="table-auto w-full">
|
||||||
|
<thead className="bg-[#f9f9fb]">
|
||||||
|
<tr className="text-left uppercase text-xs border-gray-100 border-b border-t">
|
||||||
|
<th className="px-4 py-2" colSpan={2}>
|
||||||
|
{t('sidebar.download.table_columns.name', 'Name')}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{downloadRows}</tbody>
|
||||||
|
</table>
|
||||||
|
</SidebarSection>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SidebarAlbumDownload
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { MessageState } from '../messages/Messages'
|
import { MessageState } from '../messages/Messages'
|
||||||
import { useLazyQuery, gql } from '@apollo/client'
|
import { useLazyQuery, gql } from '@apollo/client'
|
||||||
import { authToken } from '../../helpers/authentication'
|
import { authToken } from '../../helpers/authentication'
|
||||||
@@ -187,11 +186,72 @@ const downloadBlob = async (blob: Blob, filename: string) => {
|
|||||||
window.URL.revokeObjectURL(objectUrl)
|
window.URL.revokeObjectURL(objectUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SidebarDownladProps = {
|
type SidebarDownloadTableRow = {
|
||||||
|
title: string
|
||||||
|
url: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
fileSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type SidebarDownloadTableProps = {
|
||||||
|
rows: SidebarDownloadTableRow[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarDownloadTable = ({ rows }: SidebarDownloadTableProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const extractExtension = (url: string) => {
|
||||||
|
const urlMatch = url.split(/[#?]/)
|
||||||
|
if (urlMatch == null) return
|
||||||
|
|
||||||
|
return urlMatch[0].split('.').pop()?.trim().toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
const download = downloadMedia(t)
|
||||||
|
const bytes = formatBytes(t)
|
||||||
|
const downloadRows = rows.map(x => (
|
||||||
|
<tr
|
||||||
|
className="cursor-pointer border-gray-100 border-b hover:bg-gray-50 focus:bg-gray-50"
|
||||||
|
key={x.url}
|
||||||
|
onClick={() => download(x.url)}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<td className="pl-4 py-2">{`${x.title}`}</td>
|
||||||
|
<td className="py-2">{`${x.width} x ${x.height}`}</td>
|
||||||
|
<td className="py-2">{`${bytes(x.fileSize)}`}</td>
|
||||||
|
<td className="pr-4 py-2">{extractExtension(x.url)}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className="table-fixed w-full">
|
||||||
|
<thead className="bg-[#f9f9fb]">
|
||||||
|
<tr className="text-left uppercase text-xs border-gray-100 border-b border-t">
|
||||||
|
<th className="w-2/6 pl-4 py-2">
|
||||||
|
{t('sidebar.download.table_columns.name', 'Name')}
|
||||||
|
</th>
|
||||||
|
<th className="w-2/6 py-2">
|
||||||
|
{t('sidebar.download.table_columns.dimensions', 'Dimensions')}
|
||||||
|
</th>
|
||||||
|
<th className="w-1/6 py-2">
|
||||||
|
{t('sidebar.download.table_columns.file_size', 'Size')}
|
||||||
|
</th>
|
||||||
|
<th className="w-1/6 pr-4 py-2">
|
||||||
|
{t('sidebar.download.table_columns.file_type', 'Type')}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{downloadRows}</tbody>
|
||||||
|
</table>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SidebarMediaDownladProps = {
|
||||||
media: MediaSidebarMedia
|
media: MediaSidebarMedia
|
||||||
}
|
}
|
||||||
|
|
||||||
const SidebarDownload = ({ media }: SidebarDownladProps) => {
|
const SidebarMediaDownload = ({ media }: SidebarMediaDownladProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
if (!media || !media.id) return null
|
if (!media || !media.id) return null
|
||||||
|
|
||||||
@@ -214,28 +274,13 @@ const SidebarDownload = ({ media }: SidebarDownladProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractExtension = (url: string) => {
|
const downloadRows = downloads.map<SidebarDownloadTableRow>(x => ({
|
||||||
const urlMatch = url.split(/[#?]/)
|
title: x.title,
|
||||||
if (urlMatch == null) return
|
url: x.mediaUrl.url,
|
||||||
|
width: x.mediaUrl.width,
|
||||||
return urlMatch[0].split('.').pop()?.trim().toLowerCase()
|
height: x.mediaUrl.height,
|
||||||
}
|
fileSize: x.mediaUrl.fileSize,
|
||||||
|
}))
|
||||||
const download = downloadMedia(t)
|
|
||||||
const bytes = formatBytes(t)
|
|
||||||
const downloadRows = downloads.map(x => (
|
|
||||||
<tr
|
|
||||||
className="cursor-pointer border-gray-100 border-b hover:bg-gray-50 focus:bg-gray-50"
|
|
||||||
key={x.mediaUrl.url}
|
|
||||||
onClick={() => download(x.mediaUrl.url)}
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<td className="pl-4 py-2">{`${x.title}`}</td>
|
|
||||||
<td className="py-2">{`${x.mediaUrl.width} x ${x.mediaUrl.height}`}</td>
|
|
||||||
<td className="py-2">{`${bytes(x.mediaUrl.fileSize)}`}</td>
|
|
||||||
<td className="pr-4 py-2">{extractExtension(x.mediaUrl.url)}</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarSection>
|
<SidebarSection>
|
||||||
@@ -243,31 +288,9 @@ const SidebarDownload = ({ media }: SidebarDownladProps) => {
|
|||||||
{t('sidebar.download.title', 'Download')}
|
{t('sidebar.download.title', 'Download')}
|
||||||
</SidebarSectionTitle>
|
</SidebarSectionTitle>
|
||||||
|
|
||||||
<table className="table-fixed w-full">
|
<SidebarDownloadTable rows={downloadRows} />
|
||||||
<thead className="bg-[#f9f9fb]">
|
|
||||||
<tr className="text-left uppercase text-xs border-gray-100 border-b border-t">
|
|
||||||
<th className="w-2/6 pl-4 py-2">
|
|
||||||
{t('sidebar.download.table_columns.name', 'Name')}
|
|
||||||
</th>
|
|
||||||
<th className="w-2/6 py-2">
|
|
||||||
{t('sidebar.download.table_columns.dimensions', 'Dimensions')}
|
|
||||||
</th>
|
|
||||||
<th className="w-1/6 py-2">
|
|
||||||
{t('sidebar.download.table_columns.file_size', 'Size')}
|
|
||||||
</th>
|
|
||||||
<th className="w-1/6 pr-4 py-2">
|
|
||||||
{t('sidebar.download.table_columns.file_type', 'Type')}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>{downloadRows}</tbody>
|
|
||||||
</table>
|
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SidebarDownload.propTypes = {
|
export default SidebarMediaDownload
|
||||||
photo: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SidebarDownload
|
|
||||||
@@ -62,10 +62,10 @@
|
|||||||
},
|
},
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": null,
|
"change_label": "Ændre navn",
|
||||||
"detach_face": null,
|
"detach_face": "Løsriv billeder",
|
||||||
"merge_face": null,
|
"merge_face": "Sammenflet personer",
|
||||||
"move_faces": null
|
"move_faces": "Flyt ansigter"
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
"label_placeholder": "Navn",
|
"label_placeholder": "Navn",
|
||||||
@@ -215,10 +215,27 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"album_cover": null,
|
"album_cover": "Album coverbillede",
|
||||||
"cover_photo": null,
|
"download": {
|
||||||
"reset_cover": null,
|
"high-resolutions": {
|
||||||
"set_cover": null,
|
"description": "Høj opløsning JPEGs af RAW-billeder",
|
||||||
|
"title": "Høj opløsning"
|
||||||
|
},
|
||||||
|
"originals": {
|
||||||
|
"description": "De originale billeder og video",
|
||||||
|
"title": "Originaler"
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"description": "Billeder i lav opløsning, ingen videoer",
|
||||||
|
"title": "Thumbnails"
|
||||||
|
},
|
||||||
|
"web-videos": {
|
||||||
|
"description": "Videoer som er blevet optimeret til web",
|
||||||
|
"title": "Konverterede videoer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reset_cover": "Nulstil coverbillede",
|
||||||
|
"set_cover": "Set som album coverbillede",
|
||||||
"title_placeholder": "Albumtitel"
|
"title_placeholder": "Albumtitel"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
@@ -226,13 +243,13 @@
|
|||||||
"byte": "{{count}} Byte",
|
"byte": "{{count}} Byte",
|
||||||
"byte_plural": "{{count}} Bytes",
|
"byte_plural": "{{count}} Bytes",
|
||||||
"giga_byte": "{{count}} GB",
|
"giga_byte": "{{count}} GB",
|
||||||
"giga_byte_plural": null,
|
"giga_byte_plural": "",
|
||||||
"kilo_byte": "{{count}} KB",
|
"kilo_byte": "{{count}} KB",
|
||||||
"kilo_byte_plural": null,
|
"kilo_byte_plural": "",
|
||||||
"mega_byte": "{{count}} MB",
|
"mega_byte": "{{count}} MB",
|
||||||
"mega_byte_plural": null,
|
"mega_byte_plural": "",
|
||||||
"tera_byte": "{{count}} TB",
|
"tera_byte": "{{count}} TB",
|
||||||
"tera_byte_plural": null
|
"tera_byte_plural": ""
|
||||||
},
|
},
|
||||||
"table_columns": {
|
"table_columns": {
|
||||||
"dimensions": "Dimension",
|
"dimensions": "Dimension",
|
||||||
@@ -301,8 +318,8 @@
|
|||||||
},
|
},
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": null,
|
"dropdown_all": "Fra i dag",
|
||||||
"label": null
|
"label": "Dato"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
|
|||||||
@@ -55,7 +55,11 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"title": "Indstillinger for album"
|
"title": "Indstillinger for album",
|
||||||
|
"album_cover": null,
|
||||||
|
"cover_photo": "",
|
||||||
|
"reset_cover": null,
|
||||||
|
"set_cover": null
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"table_header": "Offentlige delinger"
|
"table_header": "Offentlige delinger"
|
||||||
@@ -68,5 +72,11 @@
|
|||||||
"tera_byte_plural": null
|
"tera_byte_plural": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"timeline_filter": {
|
||||||
|
"date": {
|
||||||
|
"dropdown_all": null,
|
||||||
|
"label": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"album_filter": {
|
"album_filter": {
|
||||||
"only_favorites": "Nur Favoriten anzeigen",
|
"only_favorites": "Nur Favoriten anzeigen",
|
||||||
"sort": null,
|
"sort": "",
|
||||||
"sorting_options": {
|
"sorting_options": {
|
||||||
"date_imported": "Importdatum",
|
"date_imported": "Importdatum",
|
||||||
"date_shot": "Aufnahmedatum",
|
"date_shot": "Aufnahmedatum",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"placeholder": "Suche",
|
"placeholder": "Suche",
|
||||||
"result_type": {
|
"result_type": {
|
||||||
"albums": "Alben",
|
"albums": "Alben",
|
||||||
"media": null
|
"media": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -62,54 +62,54 @@
|
|||||||
},
|
},
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": null,
|
"change_label": "",
|
||||||
"detach_face": null,
|
"detach_face": "",
|
||||||
"merge_face": null,
|
"merge_face": "",
|
||||||
"move_faces": null
|
"move_faces": ""
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
"label_placeholder": "Zuordnung",
|
"label_placeholder": "Zuordnung",
|
||||||
"unlabeled": "Nicht zugeordnet",
|
"unlabeled": "Nicht zugeordnet",
|
||||||
"unlabeled_person": null
|
"unlabeled_person": ""
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"action": {
|
"action": {
|
||||||
"merge": null
|
"merge": ""
|
||||||
},
|
},
|
||||||
"detach_image_faces": {
|
"detach_image_faces": {
|
||||||
"action": {
|
"action": {
|
||||||
"detach": null,
|
"detach": "",
|
||||||
"select_images": null
|
"select_images": ""
|
||||||
},
|
},
|
||||||
"description": null,
|
"description": "",
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"merge_face_groups": {
|
"merge_face_groups": {
|
||||||
"description": null,
|
"description": "",
|
||||||
"destination_table": {
|
"destination_table": {
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"move_image_faces": {
|
"move_image_faces": {
|
||||||
"description": null,
|
"description": "",
|
||||||
"destination_face_group_table": {
|
"destination_face_group_table": {
|
||||||
"move_action": null,
|
"move_action": "",
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"image_select_table": {
|
"image_select_table": {
|
||||||
"next_action": null,
|
"next_action": "",
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"title": null
|
"title": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"recognize_unlabeled_faces_button": "Nicht zugeordnete Gesichter erkennen",
|
"recognize_unlabeled_faces_button": "Nicht zugeordnete Gesichter erkennen",
|
||||||
"tableselect_face_group": {
|
"tableselect_face_group": {
|
||||||
"search_faces_placeholder": null
|
"search_faces_placeholder": ""
|
||||||
},
|
},
|
||||||
"tableselect_image_faces": {
|
"tableselect_image_faces": {
|
||||||
"search_images_placeholder": null
|
"search_images_placeholder": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"photos_page": {
|
"photos_page": {
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
"table": {
|
"table": {
|
||||||
"column_names": {
|
"column_names": {
|
||||||
"action": "Aktion",
|
"action": "Aktion",
|
||||||
"capabilities": null,
|
"capabilities": "",
|
||||||
"photo_path": "Pfad der Medien",
|
"photo_path": "Pfad der Medien",
|
||||||
"username": "Benutzername"
|
"username": "Benutzername"
|
||||||
},
|
},
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
},
|
},
|
||||||
"protected_share": {
|
"protected_share": {
|
||||||
"description": "Diese Freigabe ist passwortgeschützt.",
|
"description": "Diese Freigabe ist passwortgeschützt.",
|
||||||
"password_required_error": null,
|
"password_required_error": "",
|
||||||
"title": "Passwortgeschützte Freigabe"
|
"title": "Passwortgeschützte Freigabe"
|
||||||
},
|
},
|
||||||
"share_not_found": "Freigabe nicht gefunden",
|
"share_not_found": "Freigabe nicht gefunden",
|
||||||
@@ -215,24 +215,41 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"album_cover": null,
|
"album_cover": "",
|
||||||
"cover_photo": null,
|
"download": {
|
||||||
"reset_cover": null,
|
"high-resolutions": {
|
||||||
"set_cover": null,
|
"description": "",
|
||||||
"title_placeholder": null
|
"title": ""
|
||||||
|
},
|
||||||
|
"originals": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"web-videos": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reset_cover": "",
|
||||||
|
"set_cover": "",
|
||||||
|
"title_placeholder": ""
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"filesize": {
|
"filesize": {
|
||||||
"byte": "{{count}} Byte",
|
"byte": "{{count}} Byte",
|
||||||
"byte_plural": "{{count}} Bytes",
|
"byte_plural": "{{count}} Bytes",
|
||||||
"giga_byte": "{{count}} GB",
|
"giga_byte": "{{count}} GB",
|
||||||
"giga_byte_plural": null,
|
"giga_byte_plural": "",
|
||||||
"kilo_byte": "{{count}} KB",
|
"kilo_byte": "{{count}} KB",
|
||||||
"kilo_byte_plural": null,
|
"kilo_byte_plural": "",
|
||||||
"mega_byte": "{{count}} MB",
|
"mega_byte": "{{count}} MB",
|
||||||
"mega_byte_plural": null,
|
"mega_byte_plural": "",
|
||||||
"tera_byte": "{{count}} TB",
|
"tera_byte": "{{count}} TB",
|
||||||
"tera_byte_plural": null
|
"tera_byte_plural": ""
|
||||||
},
|
},
|
||||||
"table_columns": {
|
"table_columns": {
|
||||||
"dimensions": "Dimension",
|
"dimensions": "Dimension",
|
||||||
@@ -285,8 +302,8 @@
|
|||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Freigabe hinzufügen",
|
"add_share": "Freigabe hinzufügen",
|
||||||
"copy_link": "Link kopieren",
|
"copy_link": "Link kopieren",
|
||||||
"delete": null,
|
"delete": "",
|
||||||
"more": null,
|
"more": "",
|
||||||
"no_shares_found": "Keine Freigaben gefunden",
|
"no_shares_found": "Keine Freigaben gefunden",
|
||||||
"public_link": "Öffentlicher Link",
|
"public_link": "Öffentlicher Link",
|
||||||
"title": "Freigabeoptionen"
|
"title": "Freigabeoptionen"
|
||||||
@@ -301,8 +318,8 @@
|
|||||||
},
|
},
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": null,
|
"dropdown_all": "",
|
||||||
"label": null
|
"label": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
|
|||||||
@@ -97,7 +97,11 @@
|
|||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"title": "Album Optionen",
|
"title": "Album Optionen",
|
||||||
"title_placeholder": null
|
"title_placeholder": null,
|
||||||
|
"album_cover": null,
|
||||||
|
"cover_photo": "",
|
||||||
|
"reset_cover": null,
|
||||||
|
"set_cover": null
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"table_header": "Öffentliche Freigabe",
|
"table_header": "Öffentliche Freigabe",
|
||||||
@@ -120,5 +124,11 @@
|
|||||||
"protected_share": {
|
"protected_share": {
|
||||||
"password_required_error": null
|
"password_required_error": null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"timeline_filter": {
|
||||||
|
"date": {
|
||||||
|
"dropdown_all": null,
|
||||||
|
"label": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,7 +216,24 @@
|
|||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"album_cover": "Album cover",
|
"album_cover": "Album cover",
|
||||||
"cover_photo": "Album cover",
|
"download": {
|
||||||
|
"high-resolutions": {
|
||||||
|
"description": "High resolution jpegs of RAW images",
|
||||||
|
"title": "High resolutions"
|
||||||
|
},
|
||||||
|
"originals": {
|
||||||
|
"description": "The original images and videos",
|
||||||
|
"title": "Originals"
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"description": "Low resolution images, no videos",
|
||||||
|
"title": "Thumbnails"
|
||||||
|
},
|
||||||
|
"web-videos": {
|
||||||
|
"description": "Videos that have been optimized for web",
|
||||||
|
"title": "Converted videos"
|
||||||
|
}
|
||||||
|
},
|
||||||
"reset_cover": "Reset cover photo",
|
"reset_cover": "Reset cover photo",
|
||||||
"set_cover": "Set as album cover photo",
|
"set_cover": "Set as album cover photo",
|
||||||
"title_placeholder": "Album title"
|
"title_placeholder": "Album title"
|
||||||
|
|||||||
@@ -49,7 +49,8 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"title": "Album options"
|
"title": "Album options",
|
||||||
|
"cover_photo": "Album cover"
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"table_header": "Public shares"
|
"table_header": "Public shares"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"album_filter": {
|
"album_filter": {
|
||||||
"only_favorites": "Solo mostrar favoritos",
|
"only_favorites": "Solo mostrar favoritos",
|
||||||
"sort": null,
|
"sort": "",
|
||||||
"sorting_options": {
|
"sorting_options": {
|
||||||
"date_imported": "Fecha de importado",
|
"date_imported": "Fecha de importado",
|
||||||
"date_shot": "Fecha de la foto",
|
"date_shot": "Fecha de la foto",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"placeholder": "Buscar",
|
"placeholder": "Buscar",
|
||||||
"result_type": {
|
"result_type": {
|
||||||
"albums": "Álbumes",
|
"albums": "Álbumes",
|
||||||
"media": null
|
"media": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -62,54 +62,54 @@
|
|||||||
},
|
},
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": null,
|
"change_label": "",
|
||||||
"detach_face": null,
|
"detach_face": "",
|
||||||
"merge_face": null,
|
"merge_face": "",
|
||||||
"move_faces": null
|
"move_faces": ""
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
"label_placeholder": "Etiqueta",
|
"label_placeholder": "Etiqueta",
|
||||||
"unlabeled": "Sin etiquetar",
|
"unlabeled": "Sin etiquetar",
|
||||||
"unlabeled_person": null
|
"unlabeled_person": ""
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"action": {
|
"action": {
|
||||||
"merge": null
|
"merge": ""
|
||||||
},
|
},
|
||||||
"detach_image_faces": {
|
"detach_image_faces": {
|
||||||
"action": {
|
"action": {
|
||||||
"detach": null,
|
"detach": "",
|
||||||
"select_images": null
|
"select_images": ""
|
||||||
},
|
},
|
||||||
"description": null,
|
"description": "",
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"merge_face_groups": {
|
"merge_face_groups": {
|
||||||
"description": null,
|
"description": "",
|
||||||
"destination_table": {
|
"destination_table": {
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"move_image_faces": {
|
"move_image_faces": {
|
||||||
"description": null,
|
"description": "",
|
||||||
"destination_face_group_table": {
|
"destination_face_group_table": {
|
||||||
"move_action": null,
|
"move_action": "",
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"image_select_table": {
|
"image_select_table": {
|
||||||
"next_action": null,
|
"next_action": "",
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"title": null
|
"title": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"recognize_unlabeled_faces_button": "Reconocer caras sin etiquetar",
|
"recognize_unlabeled_faces_button": "Reconocer caras sin etiquetar",
|
||||||
"tableselect_face_group": {
|
"tableselect_face_group": {
|
||||||
"search_faces_placeholder": null
|
"search_faces_placeholder": ""
|
||||||
},
|
},
|
||||||
"tableselect_image_faces": {
|
"tableselect_image_faces": {
|
||||||
"search_images_placeholder": null
|
"search_images_placeholder": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"photos_page": {
|
"photos_page": {
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
"table": {
|
"table": {
|
||||||
"column_names": {
|
"column_names": {
|
||||||
"action": "Acción",
|
"action": "Acción",
|
||||||
"capabilities": null,
|
"capabilities": "",
|
||||||
"photo_path": "Ruta de las fotos",
|
"photo_path": "Ruta de las fotos",
|
||||||
"username": "Usuario"
|
"username": "Usuario"
|
||||||
},
|
},
|
||||||
@@ -195,9 +195,9 @@
|
|||||||
"title": "Usuarios"
|
"title": "Usuarios"
|
||||||
},
|
},
|
||||||
"version_info": {
|
"version_info": {
|
||||||
"build_date_title": null,
|
"build_date_title": "",
|
||||||
"title": null,
|
"title": "",
|
||||||
"version_title": null
|
"version_title": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"share_page": {
|
"share_page": {
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
},
|
},
|
||||||
"protected_share": {
|
"protected_share": {
|
||||||
"description": "Esta compartición está protegida por contraseña.",
|
"description": "Esta compartición está protegida por contraseña.",
|
||||||
"password_required_error": null,
|
"password_required_error": "",
|
||||||
"title": "Compartición protegida"
|
"title": "Compartición protegida"
|
||||||
},
|
},
|
||||||
"share_not_found": "Compartición no encontrada",
|
"share_not_found": "Compartición no encontrada",
|
||||||
@@ -215,24 +215,41 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"album_cover": null,
|
"album_cover": "",
|
||||||
"cover_photo": null,
|
"download": {
|
||||||
"reset_cover": null,
|
"high-resolutions": {
|
||||||
"set_cover": null,
|
"description": "",
|
||||||
"title_placeholder": null
|
"title": ""
|
||||||
|
},
|
||||||
|
"originals": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"web-videos": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reset_cover": "",
|
||||||
|
"set_cover": "",
|
||||||
|
"title_placeholder": ""
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"filesize": {
|
"filesize": {
|
||||||
"byte": "{{count}} Byte",
|
"byte": "{{count}} Byte",
|
||||||
"byte_plural": "{{count}} Bytes",
|
"byte_plural": "{{count}} Bytes",
|
||||||
"giga_byte": "{{count}} GB",
|
"giga_byte": "{{count}} GB",
|
||||||
"giga_byte_plural": null,
|
"giga_byte_plural": "",
|
||||||
"kilo_byte": "{{count}} KB",
|
"kilo_byte": "{{count}} KB",
|
||||||
"kilo_byte_plural": null,
|
"kilo_byte_plural": "",
|
||||||
"mega_byte": "{{count}} MB",
|
"mega_byte": "{{count}} MB",
|
||||||
"mega_byte_plural": null,
|
"mega_byte_plural": "",
|
||||||
"tera_byte": "{{count}} TB",
|
"tera_byte": "{{count}} TB",
|
||||||
"tera_byte_plural": null
|
"tera_byte_plural": ""
|
||||||
},
|
},
|
||||||
"table_columns": {
|
"table_columns": {
|
||||||
"dimensions": "Dimensiones",
|
"dimensions": "Dimensiones",
|
||||||
@@ -285,8 +302,8 @@
|
|||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Añadir compartido",
|
"add_share": "Añadir compartido",
|
||||||
"copy_link": "Copiar enlace",
|
"copy_link": "Copiar enlace",
|
||||||
"delete": null,
|
"delete": "",
|
||||||
"more": null,
|
"more": "",
|
||||||
"no_shares_found": "No se encontraron compartidos",
|
"no_shares_found": "No se encontraron compartidos",
|
||||||
"public_link": "Enlace público",
|
"public_link": "Enlace público",
|
||||||
"title": "Opciones de compartir"
|
"title": "Opciones de compartir"
|
||||||
@@ -301,13 +318,13 @@
|
|||||||
},
|
},
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": null,
|
"dropdown_all": "",
|
||||||
"label": null
|
"label": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"loading_album": "Cargando álbum",
|
"loading_album": "Cargando álbum",
|
||||||
"login": null,
|
"login": "",
|
||||||
"people": "Personas",
|
"people": "Personas",
|
||||||
"settings": "Opciones"
|
"settings": "Opciones"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,11 @@
|
|||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"title": "opciones de álbum",
|
"title": "opciones de álbum",
|
||||||
"title_placeholder": null
|
"title_placeholder": null,
|
||||||
|
"album_cover": null,
|
||||||
|
"cover_photo": "",
|
||||||
|
"reset_cover": null,
|
||||||
|
"set_cover": null
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"table_header": "Compartidos públicos",
|
"table_header": "Compartidos públicos",
|
||||||
@@ -128,5 +132,11 @@
|
|||||||
"protected_share": {
|
"protected_share": {
|
||||||
"password_required_error": null
|
"password_required_error": null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"timeline_filter": {
|
||||||
|
"date": {
|
||||||
|
"dropdown_all": null,
|
||||||
|
"label": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,10 +215,27 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"album_cover": null,
|
"album_cover": "",
|
||||||
"cover_photo": null,
|
"download": {
|
||||||
"reset_cover": null,
|
"high-resolutions": {
|
||||||
"set_cover": null,
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"originals": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"web-videos": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reset_cover": "",
|
||||||
|
"set_cover": "",
|
||||||
"title_placeholder": "Titre de l'Album"
|
"title_placeholder": "Titre de l'Album"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
@@ -301,8 +318,8 @@
|
|||||||
},
|
},
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": null,
|
"dropdown_all": "",
|
||||||
"label": null
|
"label": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
|
|||||||
@@ -87,7 +87,11 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"title": "Paramètres de l'album"
|
"title": "Paramètres de l'album",
|
||||||
|
"album_cover": null,
|
||||||
|
"cover_photo": "",
|
||||||
|
"reset_cover": null,
|
||||||
|
"set_cover": null
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"table_header": "Partages publics"
|
"table_header": "Partages publics"
|
||||||
@@ -95,5 +99,11 @@
|
|||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"login": null
|
"login": null
|
||||||
|
},
|
||||||
|
"timeline_filter": {
|
||||||
|
"date": {
|
||||||
|
"dropdown_all": null,
|
||||||
|
"label": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"album_filter": {
|
"album_filter": {
|
||||||
"only_favorites": "Mostra solo i preferiti",
|
"only_favorites": "Mostra solo i preferiti",
|
||||||
"sort": null,
|
"sort": "",
|
||||||
"sorting_options": {
|
"sorting_options": {
|
||||||
"date_imported": "Data importazione",
|
"date_imported": "Data importazione",
|
||||||
"date_shot": "Data scatto",
|
"date_shot": "Data scatto",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"placeholder": "Cerca",
|
"placeholder": "Cerca",
|
||||||
"result_type": {
|
"result_type": {
|
||||||
"albums": "Album",
|
"albums": "Album",
|
||||||
"media": null
|
"media": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -62,10 +62,10 @@
|
|||||||
},
|
},
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": null,
|
"change_label": "",
|
||||||
"detach_face": null,
|
"detach_face": "",
|
||||||
"merge_face": null,
|
"merge_face": "",
|
||||||
"move_faces": null
|
"move_faces": ""
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
"label_placeholder": "Etichetta",
|
"label_placeholder": "Etichetta",
|
||||||
@@ -106,10 +106,10 @@
|
|||||||
},
|
},
|
||||||
"recognize_unlabeled_faces_button": "Identifica facce senza etichetta",
|
"recognize_unlabeled_faces_button": "Identifica facce senza etichetta",
|
||||||
"tableselect_face_group": {
|
"tableselect_face_group": {
|
||||||
"search_faces_placeholder": null
|
"search_faces_placeholder": ""
|
||||||
},
|
},
|
||||||
"tableselect_image_faces": {
|
"tableselect_image_faces": {
|
||||||
"search_images_placeholder": null
|
"search_images_placeholder": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"photos_page": {
|
"photos_page": {
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
"table": {
|
"table": {
|
||||||
"column_names": {
|
"column_names": {
|
||||||
"action": "Azioni",
|
"action": "Azioni",
|
||||||
"capabilities": null,
|
"capabilities": "",
|
||||||
"photo_path": "Percorso foto",
|
"photo_path": "Percorso foto",
|
||||||
"username": "Username"
|
"username": "Username"
|
||||||
},
|
},
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
},
|
},
|
||||||
"protected_share": {
|
"protected_share": {
|
||||||
"description": "Questa condivisione è protetta da una password.",
|
"description": "Questa condivisione è protetta da una password.",
|
||||||
"password_required_error": null,
|
"password_required_error": "",
|
||||||
"title": "Condivisione protetta"
|
"title": "Condivisione protetta"
|
||||||
},
|
},
|
||||||
"share_not_found": "Condivisone non trovata",
|
"share_not_found": "Condivisone non trovata",
|
||||||
@@ -215,24 +215,41 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"album_cover": null,
|
"album_cover": "",
|
||||||
"cover_photo": null,
|
"download": {
|
||||||
"reset_cover": null,
|
"high-resolutions": {
|
||||||
"set_cover": null,
|
"description": "",
|
||||||
"title_placeholder": null
|
"title": ""
|
||||||
|
},
|
||||||
|
"originals": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"web-videos": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reset_cover": "",
|
||||||
|
"set_cover": "",
|
||||||
|
"title_placeholder": ""
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"filesize": {
|
"filesize": {
|
||||||
"byte": "{{count}} Byte",
|
"byte": "{{count}} Byte",
|
||||||
"byte_plural": "{{count}} Bytes",
|
"byte_plural": "{{count}} Bytes",
|
||||||
"giga_byte": "{{count}} GB",
|
"giga_byte": "{{count}} GB",
|
||||||
"giga_byte_plural": null,
|
"giga_byte_plural": "",
|
||||||
"kilo_byte": "{{count}} KB",
|
"kilo_byte": "{{count}} KB",
|
||||||
"kilo_byte_plural": null,
|
"kilo_byte_plural": "",
|
||||||
"mega_byte": "{{count}} MB",
|
"mega_byte": "{{count}} MB",
|
||||||
"mega_byte_plural": null,
|
"mega_byte_plural": "",
|
||||||
"tera_byte": "{{count}} TB",
|
"tera_byte": "{{count}} TB",
|
||||||
"tera_byte_plural": null
|
"tera_byte_plural": ""
|
||||||
},
|
},
|
||||||
"table_columns": {
|
"table_columns": {
|
||||||
"dimensions": "Dimensioni",
|
"dimensions": "Dimensioni",
|
||||||
@@ -285,8 +302,8 @@
|
|||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Aggiungi condivisione",
|
"add_share": "Aggiungi condivisione",
|
||||||
"copy_link": "Copia il link",
|
"copy_link": "Copia il link",
|
||||||
"delete": null,
|
"delete": "",
|
||||||
"more": null,
|
"more": "",
|
||||||
"no_shares_found": "Nessuna condivisione trovata",
|
"no_shares_found": "Nessuna condivisione trovata",
|
||||||
"public_link": "Link pubblico",
|
"public_link": "Link pubblico",
|
||||||
"title": "Opzioni di condivisione"
|
"title": "Opzioni di condivisione"
|
||||||
@@ -301,8 +318,8 @@
|
|||||||
},
|
},
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": null,
|
"dropdown_all": "",
|
||||||
"label": null
|
"label": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
|
|||||||
@@ -64,7 +64,11 @@
|
|||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"title": "Opzioni Album",
|
"title": "Opzioni Album",
|
||||||
"title_placeholder": null
|
"title_placeholder": null,
|
||||||
|
"album_cover": null,
|
||||||
|
"cover_photo": "",
|
||||||
|
"reset_cover": null,
|
||||||
|
"set_cover": null
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"table_header": "Condivisioni pubbliche",
|
"table_header": "Condivisioni pubbliche",
|
||||||
@@ -87,5 +91,11 @@
|
|||||||
"protected_share": {
|
"protected_share": {
|
||||||
"password_required_error": null
|
"password_required_error": null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"timeline_filter": {
|
||||||
|
"date": {
|
||||||
|
"dropdown_all": null,
|
||||||
|
"label": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"album_filter": {
|
"album_filter": {
|
||||||
"only_favorites": "Pokaż tylko ulubione",
|
"only_favorites": "Pokaż tylko ulubione",
|
||||||
"sort": null,
|
"sort": "",
|
||||||
"sorting_options": {
|
"sorting_options": {
|
||||||
"date_imported": "Data zaimportowania",
|
"date_imported": "Data zaimportowania",
|
||||||
"date_shot": "Data wykonania",
|
"date_shot": "Data wykonania",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"placeholder": "Szukaj",
|
"placeholder": "Szukaj",
|
||||||
"result_type": {
|
"result_type": {
|
||||||
"albums": "Albumy",
|
"albums": "Albumy",
|
||||||
"media": null
|
"media": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -62,54 +62,54 @@
|
|||||||
},
|
},
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": null,
|
"change_label": "",
|
||||||
"detach_face": null,
|
"detach_face": "",
|
||||||
"merge_face": null,
|
"merge_face": "",
|
||||||
"move_faces": null
|
"move_faces": ""
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
"label_placeholder": "Etykieta",
|
"label_placeholder": "Etykieta",
|
||||||
"unlabeled": "Nieoznakowany",
|
"unlabeled": "Nieoznakowany",
|
||||||
"unlabeled_person": null
|
"unlabeled_person": ""
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"action": {
|
"action": {
|
||||||
"merge": null
|
"merge": ""
|
||||||
},
|
},
|
||||||
"detach_image_faces": {
|
"detach_image_faces": {
|
||||||
"action": {
|
"action": {
|
||||||
"detach": null,
|
"detach": "",
|
||||||
"select_images": null
|
"select_images": ""
|
||||||
},
|
},
|
||||||
"description": null,
|
"description": "",
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"merge_face_groups": {
|
"merge_face_groups": {
|
||||||
"description": null,
|
"description": "",
|
||||||
"destination_table": {
|
"destination_table": {
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"move_image_faces": {
|
"move_image_faces": {
|
||||||
"description": null,
|
"description": "",
|
||||||
"destination_face_group_table": {
|
"destination_face_group_table": {
|
||||||
"move_action": null,
|
"move_action": "",
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"image_select_table": {
|
"image_select_table": {
|
||||||
"next_action": null,
|
"next_action": "",
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"title": null
|
"title": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"recognize_unlabeled_faces_button": "Rozpoznaj nieoznakowane twarze",
|
"recognize_unlabeled_faces_button": "Rozpoznaj nieoznakowane twarze",
|
||||||
"tableselect_face_group": {
|
"tableselect_face_group": {
|
||||||
"search_faces_placeholder": null
|
"search_faces_placeholder": ""
|
||||||
},
|
},
|
||||||
"tableselect_image_faces": {
|
"tableselect_image_faces": {
|
||||||
"search_images_placeholder": null
|
"search_images_placeholder": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"photos_page": {
|
"photos_page": {
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
"table": {
|
"table": {
|
||||||
"column_names": {
|
"column_names": {
|
||||||
"action": "Akcja",
|
"action": "Akcja",
|
||||||
"capabilities": null,
|
"capabilities": "",
|
||||||
"photo_path": "Ścieżka zdjęć",
|
"photo_path": "Ścieżka zdjęć",
|
||||||
"username": "Nazwa użytkownika"
|
"username": "Nazwa użytkownika"
|
||||||
},
|
},
|
||||||
@@ -195,9 +195,9 @@
|
|||||||
"title": "Użytkownicy"
|
"title": "Użytkownicy"
|
||||||
},
|
},
|
||||||
"version_info": {
|
"version_info": {
|
||||||
"build_date_title": null,
|
"build_date_title": "",
|
||||||
"title": null,
|
"title": "",
|
||||||
"version_title": null
|
"version_title": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"share_page": {
|
"share_page": {
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
},
|
},
|
||||||
"protected_share": {
|
"protected_share": {
|
||||||
"description": "Ten udział jest chroniony hasłem.",
|
"description": "Ten udział jest chroniony hasłem.",
|
||||||
"password_required_error": null,
|
"password_required_error": "",
|
||||||
"title": "Udział chroniony"
|
"title": "Udział chroniony"
|
||||||
},
|
},
|
||||||
"share_not_found": "Nie znaleziono udziału",
|
"share_not_found": "Nie znaleziono udziału",
|
||||||
@@ -215,29 +215,46 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"album_cover": null,
|
"album_cover": "",
|
||||||
"cover_photo": null,
|
"download": {
|
||||||
"reset_cover": null,
|
"high-resolutions": {
|
||||||
"set_cover": null,
|
"description": "",
|
||||||
"title_placeholder": null
|
"title": ""
|
||||||
|
},
|
||||||
|
"originals": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"web-videos": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reset_cover": "",
|
||||||
|
"set_cover": "",
|
||||||
|
"title_placeholder": ""
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"filesize": {
|
"filesize": {
|
||||||
"byte_0": null,
|
"byte_0": "",
|
||||||
"byte_1": null,
|
"byte_1": "",
|
||||||
"byte_2": null,
|
"byte_2": "",
|
||||||
"giga_byte_0": null,
|
"giga_byte_0": "",
|
||||||
"giga_byte_1": null,
|
"giga_byte_1": "",
|
||||||
"giga_byte_2": null,
|
"giga_byte_2": "",
|
||||||
"kilo_byte_0": null,
|
"kilo_byte_0": "",
|
||||||
"kilo_byte_1": null,
|
"kilo_byte_1": "",
|
||||||
"kilo_byte_2": null,
|
"kilo_byte_2": "",
|
||||||
"mega_byte_0": null,
|
"mega_byte_0": "",
|
||||||
"mega_byte_1": null,
|
"mega_byte_1": "",
|
||||||
"mega_byte_2": null,
|
"mega_byte_2": "",
|
||||||
"tera_byte_0": null,
|
"tera_byte_0": "",
|
||||||
"tera_byte_1": null,
|
"tera_byte_1": "",
|
||||||
"tera_byte_2": null
|
"tera_byte_2": ""
|
||||||
},
|
},
|
||||||
"table_columns": {
|
"table_columns": {
|
||||||
"dimensions": "Wymiary",
|
"dimensions": "Wymiary",
|
||||||
@@ -290,8 +307,8 @@
|
|||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Dodaj udział",
|
"add_share": "Dodaj udział",
|
||||||
"copy_link": "Skopiuj link",
|
"copy_link": "Skopiuj link",
|
||||||
"delete": null,
|
"delete": "",
|
||||||
"more": null,
|
"more": "",
|
||||||
"no_shares_found": "Nie znaleziono udostępnionych",
|
"no_shares_found": "Nie znaleziono udostępnionych",
|
||||||
"public_link": "Link publiczny",
|
"public_link": "Link publiczny",
|
||||||
"title": "Opcje udostępniania"
|
"title": "Opcje udostępniania"
|
||||||
@@ -306,13 +323,13 @@
|
|||||||
},
|
},
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": null,
|
"dropdown_all": "",
|
||||||
"label": null
|
"label": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"loading_album": "Ładowanie albumu",
|
"loading_album": "Ładowanie albumu",
|
||||||
"login": null,
|
"login": "",
|
||||||
"people": "Ludzie",
|
"people": "Ludzie",
|
||||||
"settings": "Ustawienia"
|
"settings": "Ustawienia"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,11 @@
|
|||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"title": "Opcje albumu",
|
"title": "Opcje albumu",
|
||||||
"title_placeholder": null
|
"title_placeholder": null,
|
||||||
|
"album_cover": null,
|
||||||
|
"cover_photo": "",
|
||||||
|
"reset_cover": null,
|
||||||
|
"set_cover": null
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"filesize": {
|
"filesize": {
|
||||||
@@ -145,5 +149,11 @@
|
|||||||
"protected_share": {
|
"protected_share": {
|
||||||
"password_required_error": null
|
"password_required_error": null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"timeline_filter": {
|
||||||
|
"date": {
|
||||||
|
"dropdown_all": null,
|
||||||
|
"label": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"album_filter": {
|
"album_filter": {
|
||||||
"only_favorites": "Показать только избранные",
|
"only_favorites": "Показать только избранные",
|
||||||
"sort": null,
|
"sort": "",
|
||||||
"sorting_options": {
|
"sorting_options": {
|
||||||
"date_imported": "Дата импортирования",
|
"date_imported": "Дата импортирования",
|
||||||
"date_shot": "Дата снимка",
|
"date_shot": "Дата снимка",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"placeholder": "Поиск",
|
"placeholder": "Поиск",
|
||||||
"result_type": {
|
"result_type": {
|
||||||
"albums": "Альбомы",
|
"albums": "Альбомы",
|
||||||
"media": null
|
"media": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -62,10 +62,10 @@
|
|||||||
},
|
},
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": null,
|
"change_label": "",
|
||||||
"detach_face": null,
|
"detach_face": "",
|
||||||
"merge_face": null,
|
"merge_face": "",
|
||||||
"move_faces": null
|
"move_faces": ""
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
"label_placeholder": "Метка",
|
"label_placeholder": "Метка",
|
||||||
@@ -106,10 +106,10 @@
|
|||||||
},
|
},
|
||||||
"recognize_unlabeled_faces_button": "Распознавать непомеченные лица",
|
"recognize_unlabeled_faces_button": "Распознавать непомеченные лица",
|
||||||
"tableselect_face_group": {
|
"tableselect_face_group": {
|
||||||
"search_faces_placeholder": null
|
"search_faces_placeholder": ""
|
||||||
},
|
},
|
||||||
"tableselect_image_faces": {
|
"tableselect_image_faces": {
|
||||||
"search_images_placeholder": null
|
"search_images_placeholder": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"photos_page": {
|
"photos_page": {
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
"table": {
|
"table": {
|
||||||
"column_names": {
|
"column_names": {
|
||||||
"action": "Действие",
|
"action": "Действие",
|
||||||
"capabilities": null,
|
"capabilities": "",
|
||||||
"photo_path": "Путь к фото",
|
"photo_path": "Путь к фото",
|
||||||
"username": "Имя пользователя"
|
"username": "Имя пользователя"
|
||||||
},
|
},
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
},
|
},
|
||||||
"protected_share": {
|
"protected_share": {
|
||||||
"description": "Это общее медиа защищено паролем.",
|
"description": "Это общее медиа защищено паролем.",
|
||||||
"password_required_error": null,
|
"password_required_error": "",
|
||||||
"title": "Защищённое медиа"
|
"title": "Защищённое медиа"
|
||||||
},
|
},
|
||||||
"share_not_found": "Общее медиа не найдено",
|
"share_not_found": "Общее медиа не найдено",
|
||||||
@@ -215,29 +215,46 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"album_cover": null,
|
"album_cover": "",
|
||||||
"cover_photo": null,
|
"download": {
|
||||||
"reset_cover": null,
|
"high-resolutions": {
|
||||||
"set_cover": null,
|
"description": "",
|
||||||
"title_placeholder": null
|
"title": ""
|
||||||
|
},
|
||||||
|
"originals": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"web-videos": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reset_cover": "",
|
||||||
|
"set_cover": "",
|
||||||
|
"title_placeholder": ""
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"filesize": {
|
"filesize": {
|
||||||
"byte_0": null,
|
"byte_0": "",
|
||||||
"byte_1": null,
|
"byte_1": "",
|
||||||
"byte_2": null,
|
"byte_2": "",
|
||||||
"giga_byte_0": null,
|
"giga_byte_0": "",
|
||||||
"giga_byte_1": null,
|
"giga_byte_1": "",
|
||||||
"giga_byte_2": null,
|
"giga_byte_2": "",
|
||||||
"kilo_byte_0": null,
|
"kilo_byte_0": "",
|
||||||
"kilo_byte_1": null,
|
"kilo_byte_1": "",
|
||||||
"kilo_byte_2": null,
|
"kilo_byte_2": "",
|
||||||
"mega_byte_0": null,
|
"mega_byte_0": "",
|
||||||
"mega_byte_1": null,
|
"mega_byte_1": "",
|
||||||
"mega_byte_2": null,
|
"mega_byte_2": "",
|
||||||
"tera_byte_0": null,
|
"tera_byte_0": "",
|
||||||
"tera_byte_1": null,
|
"tera_byte_1": "",
|
||||||
"tera_byte_2": null
|
"tera_byte_2": ""
|
||||||
},
|
},
|
||||||
"table_columns": {
|
"table_columns": {
|
||||||
"dimensions": "Габариты",
|
"dimensions": "Габариты",
|
||||||
@@ -290,8 +307,8 @@
|
|||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Поделится",
|
"add_share": "Поделится",
|
||||||
"copy_link": "Скопировать ссылку",
|
"copy_link": "Скопировать ссылку",
|
||||||
"delete": null,
|
"delete": "",
|
||||||
"more": null,
|
"more": "",
|
||||||
"no_shares_found": "Нет доступа",
|
"no_shares_found": "Нет доступа",
|
||||||
"public_link": "Общедоступная ссылка",
|
"public_link": "Общедоступная ссылка",
|
||||||
"title": "Настройки доступа"
|
"title": "Настройки доступа"
|
||||||
@@ -306,8 +323,8 @@
|
|||||||
},
|
},
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": null,
|
"dropdown_all": "",
|
||||||
"label": null
|
"label": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
|
|||||||
@@ -64,7 +64,11 @@
|
|||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"title": "Свойства альбома",
|
"title": "Свойства альбома",
|
||||||
"title_placeholder": null
|
"title_placeholder": null,
|
||||||
|
"album_cover": null,
|
||||||
|
"cover_photo": "",
|
||||||
|
"reset_cover": null,
|
||||||
|
"set_cover": null
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"filesize": {
|
"filesize": {
|
||||||
@@ -104,5 +108,11 @@
|
|||||||
"protected_share": {
|
"protected_share": {
|
||||||
"password_required_error": null
|
"password_required_error": null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"timeline_filter": {
|
||||||
|
"date": {
|
||||||
|
"dropdown_all": null,
|
||||||
|
"label": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"album_filter": {
|
"album_filter": {
|
||||||
"only_favorites": "Visa endast favoriter",
|
"only_favorites": "Visa endast favoriter",
|
||||||
"sort": null,
|
"sort": "",
|
||||||
"sorting_options": {
|
"sorting_options": {
|
||||||
"date_imported": "Datum för import",
|
"date_imported": "Datum för import",
|
||||||
"date_shot": "Datum",
|
"date_shot": "Datum",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"placeholder": "Sök",
|
"placeholder": "Sök",
|
||||||
"result_type": {
|
"result_type": {
|
||||||
"albums": "Album",
|
"albums": "Album",
|
||||||
"media": null
|
"media": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -62,61 +62,61 @@
|
|||||||
},
|
},
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": null,
|
"change_label": "",
|
||||||
"detach_face": null,
|
"detach_face": "",
|
||||||
"merge_face": null,
|
"merge_face": "",
|
||||||
"move_faces": null
|
"move_faces": ""
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
"label_placeholder": "Märkning",
|
"label_placeholder": "Märkning",
|
||||||
"unlabeled": "Omärkt",
|
"unlabeled": "Omärkt",
|
||||||
"unlabeled_person": null
|
"unlabeled_person": ""
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"action": {
|
"action": {
|
||||||
"merge": null
|
"merge": ""
|
||||||
},
|
},
|
||||||
"detach_image_faces": {
|
"detach_image_faces": {
|
||||||
"action": {
|
"action": {
|
||||||
"detach": null,
|
"detach": "",
|
||||||
"select_images": null
|
"select_images": ""
|
||||||
},
|
},
|
||||||
"description": null,
|
"description": "",
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"merge_face_groups": {
|
"merge_face_groups": {
|
||||||
"description": null,
|
"description": "",
|
||||||
"destination_table": {
|
"destination_table": {
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"move_image_faces": {
|
"move_image_faces": {
|
||||||
"description": null,
|
"description": "",
|
||||||
"destination_face_group_table": {
|
"destination_face_group_table": {
|
||||||
"move_action": null,
|
"move_action": "",
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"image_select_table": {
|
"image_select_table": {
|
||||||
"next_action": null,
|
"next_action": "",
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"title": null
|
"title": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"recognize_unlabeled_faces_button": "Känna igen omärkta ansikten",
|
"recognize_unlabeled_faces_button": "Känna igen omärkta ansikten",
|
||||||
"tableselect_face_group": {
|
"tableselect_face_group": {
|
||||||
"search_faces_placeholder": null
|
"search_faces_placeholder": ""
|
||||||
},
|
},
|
||||||
"tableselect_image_faces": {
|
"tableselect_image_faces": {
|
||||||
"search_images_placeholder": null
|
"search_images_placeholder": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"photos_page": {
|
"photos_page": {
|
||||||
"title": "Bilder"
|
"title": "Bilder"
|
||||||
},
|
},
|
||||||
"places_page": {
|
"places_page": {
|
||||||
"title": null
|
"title": ""
|
||||||
},
|
},
|
||||||
"routes": {
|
"routes": {
|
||||||
"page_not_found": "Sidan hittades inte"
|
"page_not_found": "Sidan hittades inte"
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
"table": {
|
"table": {
|
||||||
"column_names": {
|
"column_names": {
|
||||||
"action": "Åtgärd",
|
"action": "Åtgärd",
|
||||||
"capabilities": null,
|
"capabilities": "",
|
||||||
"photo_path": "Sökväg till bild",
|
"photo_path": "Sökväg till bild",
|
||||||
"username": "Användarnamn"
|
"username": "Användarnamn"
|
||||||
},
|
},
|
||||||
@@ -195,9 +195,9 @@
|
|||||||
"title": "Användare"
|
"title": "Användare"
|
||||||
},
|
},
|
||||||
"version_info": {
|
"version_info": {
|
||||||
"build_date_title": null,
|
"build_date_title": "",
|
||||||
"title": null,
|
"title": "",
|
||||||
"version_title": null
|
"version_title": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"share_page": {
|
"share_page": {
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
},
|
},
|
||||||
"protected_share": {
|
"protected_share": {
|
||||||
"description": "Denna delning är skyddad med ett lösenord.",
|
"description": "Denna delning är skyddad med ett lösenord.",
|
||||||
"password_required_error": null,
|
"password_required_error": "",
|
||||||
"title": "Skyddad delning"
|
"title": "Skyddad delning"
|
||||||
},
|
},
|
||||||
"share_not_found": "Delning hittades inte",
|
"share_not_found": "Delning hittades inte",
|
||||||
@@ -215,24 +215,41 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"album_cover": null,
|
"album_cover": "",
|
||||||
"cover_photo": null,
|
"download": {
|
||||||
"reset_cover": null,
|
"high-resolutions": {
|
||||||
"set_cover": null,
|
"description": "",
|
||||||
"title_placeholder": null
|
"title": ""
|
||||||
|
},
|
||||||
|
"originals": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"web-videos": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reset_cover": "",
|
||||||
|
"set_cover": "",
|
||||||
|
"title_placeholder": ""
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"filesize": {
|
"filesize": {
|
||||||
"byte": "{{count}} Byte",
|
"byte": "{{count}} Byte",
|
||||||
"byte_plural": "{{count}} Bytes",
|
"byte_plural": "{{count}} Bytes",
|
||||||
"giga_byte": "{{count}} GB",
|
"giga_byte": "{{count}} GB",
|
||||||
"giga_byte_plural": null,
|
"giga_byte_plural": "",
|
||||||
"kilo_byte": "{{count}} KB",
|
"kilo_byte": "{{count}} KB",
|
||||||
"kilo_byte_plural": null,
|
"kilo_byte_plural": "",
|
||||||
"mega_byte": "{{count}} MB",
|
"mega_byte": "{{count}} MB",
|
||||||
"mega_byte_plural": null,
|
"mega_byte_plural": "",
|
||||||
"tera_byte": "{{count}} TB",
|
"tera_byte": "{{count}} TB",
|
||||||
"tera_byte_plural": null
|
"tera_byte_plural": ""
|
||||||
},
|
},
|
||||||
"table_columns": {
|
"table_columns": {
|
||||||
"dimensions": "Mått",
|
"dimensions": "Mått",
|
||||||
@@ -285,8 +302,8 @@
|
|||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Dela",
|
"add_share": "Dela",
|
||||||
"copy_link": "Kopiera länk",
|
"copy_link": "Kopiera länk",
|
||||||
"delete": null,
|
"delete": "",
|
||||||
"more": null,
|
"more": "",
|
||||||
"no_shares_found": "Inga delningar hittades",
|
"no_shares_found": "Inga delningar hittades",
|
||||||
"public_link": "Publika länkar",
|
"public_link": "Publika länkar",
|
||||||
"title": "Delningsinställningar"
|
"title": "Delningsinställningar"
|
||||||
@@ -301,13 +318,13 @@
|
|||||||
},
|
},
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": null,
|
"dropdown_all": "",
|
||||||
"label": null
|
"label": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"loading_album": "Laddar album",
|
"loading_album": "Laddar album",
|
||||||
"login": null,
|
"login": "",
|
||||||
"people": "Personer",
|
"people": "Personer",
|
||||||
"settings": "Inställningar"
|
"settings": "Inställningar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,11 @@
|
|||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
"title": "Albuminställningar",
|
"title": "Albuminställningar",
|
||||||
"title_placeholder": null
|
"title_placeholder": null,
|
||||||
|
"album_cover": null,
|
||||||
|
"cover_photo": "",
|
||||||
|
"reset_cover": null,
|
||||||
|
"set_cover": null
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"table_header": "Publika delningar",
|
"table_header": "Publika delningar",
|
||||||
@@ -131,5 +135,11 @@
|
|||||||
"protected_share": {
|
"protected_share": {
|
||||||
"password_required_error": null
|
"password_required_error": null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"timeline_filter": {
|
||||||
|
"date": {
|
||||||
|
"dropdown_all": null,
|
||||||
|
"label": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export function setupLocalization(): void {
|
|||||||
lng: 'en',
|
lng: 'en',
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
returnNull: false,
|
returnNull: false,
|
||||||
|
returnEmptyString: false,
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user