mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 19:49:16 +00:00
@@ -55,6 +55,47 @@ func AddMediaShare(db *gorm.DB, userID int, mediaID int, expire *time.Time, pass
|
||||
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) {
|
||||
token, err := getUserToken(db, userID, tokenValue)
|
||||
if err != nil {
|
||||
|
||||
@@ -102,7 +102,7 @@ func (p *MediaURL) CachedPath() (string, error) {
|
||||
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)
|
||||
} else if p.Purpose == MediaOriginal {
|
||||
cachedPath = p.Media.Path
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"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/utils"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -100,44 +99,7 @@ func (r *mutationResolver) ShareAlbum(ctx context.Context, albumID int, expire *
|
||||
return nil, auth.ErrUnauthorized
|
||||
}
|
||||
|
||||
var count int64
|
||||
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
|
||||
return actions.AddAlbumShare(r.Database, user, albumID, expire, password)
|
||||
}
|
||||
|
||||
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()
|
||||
routes.RegisterVideoRoutes(db, videoRouter)
|
||||
|
||||
downloadsRouter := endpointRouter.PathPrefix("/download").Subrouter()
|
||||
routes.RegisterDownloadRoutes(db, downloadsRouter)
|
||||
|
||||
shouldServeUI := utils.ShouldServeUI()
|
||||
|
||||
if shouldServeUI {
|
||||
|
||||
@@ -13,7 +13,7 @@ import SharePage, {
|
||||
VALIDATE_TOKEN_PASSWORD_QUERY,
|
||||
} from './SharePage'
|
||||
|
||||
import { SIDEBAR_DOWNLOAD_QUERY } from '../../components/sidebar/SidebarDownload'
|
||||
import { SIDEBAR_DOWNLOAD_QUERY } from '../../components/sidebar/SidebarDownloadMedia'
|
||||
import { SHARE_ALBUM_QUERY } from './AlbumSharePage'
|
||||
|
||||
jest.mock('../../hooks/useScrollPagination')
|
||||
|
||||
@@ -17,9 +17,11 @@ import { MessageState } from './components/messages/Messages'
|
||||
import { Message } from './components/messages/SubscriptionsHook'
|
||||
import { NotificationType } from './__generated__/globalTypes'
|
||||
|
||||
export const GRAPHQL_ENDPOINT = process.env.REACT_APP_API_ENDPOINT
|
||||
? urlJoin(process.env.REACT_APP_API_ENDPOINT as string, '/graphql')
|
||||
: urlJoin(location.origin, '/api/graphql')
|
||||
export const API_ENDPOINT = process.env.REACT_APP_API_ENDPOINT
|
||||
? (process.env.REACT_APP_API_ENDPOINT as string)
|
||||
: urlJoin(location.origin, '/api')
|
||||
|
||||
export const GRAPHQL_ENDPOINT = urlJoin(API_ENDPOINT, '/graphql')
|
||||
|
||||
const httpLink = new HttpLink({
|
||||
uri: GRAPHQL_ENDPOINT,
|
||||
|
||||
@@ -65,7 +65,7 @@ export const SidebarPhotoCover = ({ cover_id }: SidebarPhotoCoverProps) => {
|
||||
return (
|
||||
<SidebarSection>
|
||||
<SidebarSectionTitle>
|
||||
{t('sidebar.album.cover_photo', 'Album cover')}
|
||||
{t('sidebar.album.album_cover', 'Album cover')}
|
||||
</SidebarSectionTitle>
|
||||
<div>
|
||||
<table className="border-collapse w-full">
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getAlbumSidebarVariables,
|
||||
} from './__generated__/getAlbumSidebar'
|
||||
import { SidebarAlbumCover } from './AlbumCovers'
|
||||
import SidebarAlbumDownload from './SidebarDownloadAlbum'
|
||||
|
||||
const albumQuery = gql`
|
||||
query getAlbumSidebar($id: ID!) {
|
||||
@@ -50,6 +51,9 @@ const AlbumSidebar = ({ albumId }: AlbumSidebarProps) => {
|
||||
<div className="mt-8">
|
||||
<SidebarAlbumCover id={albumId} />
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<SidebarAlbumDownload albumID={albumId} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ProtectedVideoProps_Media,
|
||||
} from '../photoGallery/ProtectedMedia'
|
||||
import { SidebarPhotoShare } from './Sharing'
|
||||
import SidebarDownload from './SidebarDownload'
|
||||
import SidebarMediaDownload from './SidebarDownloadMedia'
|
||||
import SidebarItem from './SidebarItem'
|
||||
import { SidebarFacesOverlay } from '../facesOverlay/FacesOverlay'
|
||||
import { isNil } from '../../helpers/utils'
|
||||
@@ -364,7 +364,7 @@ const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => {
|
||||
)}
|
||||
</div>
|
||||
<MetadataInfo media={media} />
|
||||
<SidebarDownload media={media} />
|
||||
<SidebarMediaDownload media={media} />
|
||||
<SidebarPhotoShare id={media.id} />
|
||||
<div className="mt-8">
|
||||
<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 PropTypes from 'prop-types'
|
||||
import { MessageState } from '../messages/Messages'
|
||||
import { useLazyQuery, gql } from '@apollo/client'
|
||||
import { authToken } from '../../helpers/authentication'
|
||||
@@ -187,11 +186,72 @@ const downloadBlob = async (blob: Blob, filename: string) => {
|
||||
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
|
||||
}
|
||||
|
||||
const SidebarDownload = ({ media }: SidebarDownladProps) => {
|
||||
const SidebarMediaDownload = ({ media }: SidebarMediaDownladProps) => {
|
||||
const { t } = useTranslation()
|
||||
if (!media || !media.id) return null
|
||||
|
||||
@@ -214,28 +274,13 @@ const SidebarDownload = ({ media }: SidebarDownladProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
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 = 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>
|
||||
))
|
||||
const downloadRows = downloads.map<SidebarDownloadTableRow>(x => ({
|
||||
title: x.title,
|
||||
url: x.mediaUrl.url,
|
||||
width: x.mediaUrl.width,
|
||||
height: x.mediaUrl.height,
|
||||
fileSize: x.mediaUrl.fileSize,
|
||||
}))
|
||||
|
||||
return (
|
||||
<SidebarSection>
|
||||
@@ -243,31 +288,9 @@ const SidebarDownload = ({ media }: SidebarDownladProps) => {
|
||||
{t('sidebar.download.title', 'Download')}
|
||||
</SidebarSectionTitle>
|
||||
|
||||
<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>
|
||||
<SidebarDownloadTable rows={downloadRows} />
|
||||
</SidebarSection>
|
||||
)
|
||||
}
|
||||
|
||||
SidebarDownload.propTypes = {
|
||||
photo: PropTypes.object,
|
||||
}
|
||||
|
||||
export default SidebarDownload
|
||||
export default SidebarMediaDownload
|
||||
@@ -62,10 +62,10 @@
|
||||
},
|
||||
"people_page": {
|
||||
"action_label": {
|
||||
"change_label": null,
|
||||
"detach_face": null,
|
||||
"merge_face": null,
|
||||
"move_faces": null
|
||||
"change_label": "Ændre navn",
|
||||
"detach_face": "Løsriv billeder",
|
||||
"merge_face": "Sammenflet personer",
|
||||
"move_faces": "Flyt ansigter"
|
||||
},
|
||||
"face_group": {
|
||||
"label_placeholder": "Navn",
|
||||
@@ -215,10 +215,27 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"album_cover": null,
|
||||
"cover_photo": null,
|
||||
"reset_cover": null,
|
||||
"set_cover": null,
|
||||
"album_cover": "Album coverbillede",
|
||||
"download": {
|
||||
"high-resolutions": {
|
||||
"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"
|
||||
},
|
||||
"download": {
|
||||
@@ -226,13 +243,13 @@
|
||||
"byte": "{{count}} Byte",
|
||||
"byte_plural": "{{count}} Bytes",
|
||||
"giga_byte": "{{count}} GB",
|
||||
"giga_byte_plural": null,
|
||||
"giga_byte_plural": "",
|
||||
"kilo_byte": "{{count}} KB",
|
||||
"kilo_byte_plural": null,
|
||||
"kilo_byte_plural": "",
|
||||
"mega_byte": "{{count}} MB",
|
||||
"mega_byte_plural": null,
|
||||
"mega_byte_plural": "",
|
||||
"tera_byte": "{{count}} TB",
|
||||
"tera_byte_plural": null
|
||||
"tera_byte_plural": ""
|
||||
},
|
||||
"table_columns": {
|
||||
"dimensions": "Dimension",
|
||||
@@ -301,8 +318,8 @@
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
"dropdown_all": "Fra i dag",
|
||||
"label": "Dato"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
|
||||
@@ -55,7 +55,11 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"title": "Indstillinger for album"
|
||||
"title": "Indstillinger for album",
|
||||
"album_cover": null,
|
||||
"cover_photo": "",
|
||||
"reset_cover": null,
|
||||
"set_cover": null
|
||||
},
|
||||
"sharing": {
|
||||
"table_header": "Offentlige delinger"
|
||||
@@ -68,5 +72,11 @@
|
||||
"tera_byte_plural": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"album_filter": {
|
||||
"only_favorites": "Nur Favoriten anzeigen",
|
||||
"sort": null,
|
||||
"sort": "",
|
||||
"sorting_options": {
|
||||
"date_imported": "Importdatum",
|
||||
"date_shot": "Aufnahmedatum",
|
||||
@@ -35,7 +35,7 @@
|
||||
"placeholder": "Suche",
|
||||
"result_type": {
|
||||
"albums": "Alben",
|
||||
"media": null
|
||||
"media": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -62,54 +62,54 @@
|
||||
},
|
||||
"people_page": {
|
||||
"action_label": {
|
||||
"change_label": null,
|
||||
"detach_face": null,
|
||||
"merge_face": null,
|
||||
"move_faces": null
|
||||
"change_label": "",
|
||||
"detach_face": "",
|
||||
"merge_face": "",
|
||||
"move_faces": ""
|
||||
},
|
||||
"face_group": {
|
||||
"label_placeholder": "Zuordnung",
|
||||
"unlabeled": "Nicht zugeordnet",
|
||||
"unlabeled_person": null
|
||||
"unlabeled_person": ""
|
||||
},
|
||||
"modal": {
|
||||
"action": {
|
||||
"merge": null
|
||||
"merge": ""
|
||||
},
|
||||
"detach_image_faces": {
|
||||
"action": {
|
||||
"detach": null,
|
||||
"select_images": null
|
||||
"detach": "",
|
||||
"select_images": ""
|
||||
},
|
||||
"description": null,
|
||||
"title": null
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"merge_face_groups": {
|
||||
"description": null,
|
||||
"description": "",
|
||||
"destination_table": {
|
||||
"title": null
|
||||
"title": ""
|
||||
},
|
||||
"title": null
|
||||
"title": ""
|
||||
},
|
||||
"move_image_faces": {
|
||||
"description": null,
|
||||
"description": "",
|
||||
"destination_face_group_table": {
|
||||
"move_action": null,
|
||||
"title": null
|
||||
"move_action": "",
|
||||
"title": ""
|
||||
},
|
||||
"image_select_table": {
|
||||
"next_action": null,
|
||||
"title": null
|
||||
"next_action": "",
|
||||
"title": ""
|
||||
},
|
||||
"title": null
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"recognize_unlabeled_faces_button": "Nicht zugeordnete Gesichter erkennen",
|
||||
"tableselect_face_group": {
|
||||
"search_faces_placeholder": null
|
||||
"search_faces_placeholder": ""
|
||||
},
|
||||
"tableselect_image_faces": {
|
||||
"search_images_placeholder": null
|
||||
"search_images_placeholder": ""
|
||||
}
|
||||
},
|
||||
"photos_page": {
|
||||
@@ -178,7 +178,7 @@
|
||||
"table": {
|
||||
"column_names": {
|
||||
"action": "Aktion",
|
||||
"capabilities": null,
|
||||
"capabilities": "",
|
||||
"photo_path": "Pfad der Medien",
|
||||
"username": "Benutzername"
|
||||
},
|
||||
@@ -206,7 +206,7 @@
|
||||
},
|
||||
"protected_share": {
|
||||
"description": "Diese Freigabe ist passwortgeschützt.",
|
||||
"password_required_error": null,
|
||||
"password_required_error": "",
|
||||
"title": "Passwortgeschützte Freigabe"
|
||||
},
|
||||
"share_not_found": "Freigabe nicht gefunden",
|
||||
@@ -215,24 +215,41 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"album_cover": null,
|
||||
"cover_photo": null,
|
||||
"reset_cover": null,
|
||||
"set_cover": null,
|
||||
"title_placeholder": null
|
||||
"album_cover": "",
|
||||
"download": {
|
||||
"high-resolutions": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"originals": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"thumbnails": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"web-videos": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"reset_cover": "",
|
||||
"set_cover": "",
|
||||
"title_placeholder": ""
|
||||
},
|
||||
"download": {
|
||||
"filesize": {
|
||||
"byte": "{{count}} Byte",
|
||||
"byte_plural": "{{count}} Bytes",
|
||||
"giga_byte": "{{count}} GB",
|
||||
"giga_byte_plural": null,
|
||||
"giga_byte_plural": "",
|
||||
"kilo_byte": "{{count}} KB",
|
||||
"kilo_byte_plural": null,
|
||||
"kilo_byte_plural": "",
|
||||
"mega_byte": "{{count}} MB",
|
||||
"mega_byte_plural": null,
|
||||
"mega_byte_plural": "",
|
||||
"tera_byte": "{{count}} TB",
|
||||
"tera_byte_plural": null
|
||||
"tera_byte_plural": ""
|
||||
},
|
||||
"table_columns": {
|
||||
"dimensions": "Dimension",
|
||||
@@ -285,8 +302,8 @@
|
||||
"sharing": {
|
||||
"add_share": "Freigabe hinzufügen",
|
||||
"copy_link": "Link kopieren",
|
||||
"delete": null,
|
||||
"more": null,
|
||||
"delete": "",
|
||||
"more": "",
|
||||
"no_shares_found": "Keine Freigaben gefunden",
|
||||
"public_link": "Öffentlicher Link",
|
||||
"title": "Freigabeoptionen"
|
||||
@@ -301,8 +318,8 @@
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
"dropdown_all": "",
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
|
||||
@@ -97,7 +97,11 @@
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"title": "Album Optionen",
|
||||
"title_placeholder": null
|
||||
"title_placeholder": null,
|
||||
"album_cover": null,
|
||||
"cover_photo": "",
|
||||
"reset_cover": null,
|
||||
"set_cover": null
|
||||
},
|
||||
"sharing": {
|
||||
"table_header": "Öffentliche Freigabe",
|
||||
@@ -120,5 +124,11 @@
|
||||
"protected_share": {
|
||||
"password_required_error": null
|
||||
}
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +216,24 @@
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"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",
|
||||
"set_cover": "Set as album cover photo",
|
||||
"title_placeholder": "Album title"
|
||||
|
||||
@@ -49,7 +49,8 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"title": "Album options"
|
||||
"title": "Album options",
|
||||
"cover_photo": "Album cover"
|
||||
},
|
||||
"sharing": {
|
||||
"table_header": "Public shares"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"album_filter": {
|
||||
"only_favorites": "Solo mostrar favoritos",
|
||||
"sort": null,
|
||||
"sort": "",
|
||||
"sorting_options": {
|
||||
"date_imported": "Fecha de importado",
|
||||
"date_shot": "Fecha de la foto",
|
||||
@@ -35,7 +35,7 @@
|
||||
"placeholder": "Buscar",
|
||||
"result_type": {
|
||||
"albums": "Álbumes",
|
||||
"media": null
|
||||
"media": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -62,54 +62,54 @@
|
||||
},
|
||||
"people_page": {
|
||||
"action_label": {
|
||||
"change_label": null,
|
||||
"detach_face": null,
|
||||
"merge_face": null,
|
||||
"move_faces": null
|
||||
"change_label": "",
|
||||
"detach_face": "",
|
||||
"merge_face": "",
|
||||
"move_faces": ""
|
||||
},
|
||||
"face_group": {
|
||||
"label_placeholder": "Etiqueta",
|
||||
"unlabeled": "Sin etiquetar",
|
||||
"unlabeled_person": null
|
||||
"unlabeled_person": ""
|
||||
},
|
||||
"modal": {
|
||||
"action": {
|
||||
"merge": null
|
||||
"merge": ""
|
||||
},
|
||||
"detach_image_faces": {
|
||||
"action": {
|
||||
"detach": null,
|
||||
"select_images": null
|
||||
"detach": "",
|
||||
"select_images": ""
|
||||
},
|
||||
"description": null,
|
||||
"title": null
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"merge_face_groups": {
|
||||
"description": null,
|
||||
"description": "",
|
||||
"destination_table": {
|
||||
"title": null
|
||||
"title": ""
|
||||
},
|
||||
"title": null
|
||||
"title": ""
|
||||
},
|
||||
"move_image_faces": {
|
||||
"description": null,
|
||||
"description": "",
|
||||
"destination_face_group_table": {
|
||||
"move_action": null,
|
||||
"title": null
|
||||
"move_action": "",
|
||||
"title": ""
|
||||
},
|
||||
"image_select_table": {
|
||||
"next_action": null,
|
||||
"title": null
|
||||
"next_action": "",
|
||||
"title": ""
|
||||
},
|
||||
"title": null
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"recognize_unlabeled_faces_button": "Reconocer caras sin etiquetar",
|
||||
"tableselect_face_group": {
|
||||
"search_faces_placeholder": null
|
||||
"search_faces_placeholder": ""
|
||||
},
|
||||
"tableselect_image_faces": {
|
||||
"search_images_placeholder": null
|
||||
"search_images_placeholder": ""
|
||||
}
|
||||
},
|
||||
"photos_page": {
|
||||
@@ -178,7 +178,7 @@
|
||||
"table": {
|
||||
"column_names": {
|
||||
"action": "Acción",
|
||||
"capabilities": null,
|
||||
"capabilities": "",
|
||||
"photo_path": "Ruta de las fotos",
|
||||
"username": "Usuario"
|
||||
},
|
||||
@@ -195,9 +195,9 @@
|
||||
"title": "Usuarios"
|
||||
},
|
||||
"version_info": {
|
||||
"build_date_title": null,
|
||||
"title": null,
|
||||
"version_title": null
|
||||
"build_date_title": "",
|
||||
"title": "",
|
||||
"version_title": ""
|
||||
}
|
||||
},
|
||||
"share_page": {
|
||||
@@ -206,7 +206,7 @@
|
||||
},
|
||||
"protected_share": {
|
||||
"description": "Esta compartición está protegida por contraseña.",
|
||||
"password_required_error": null,
|
||||
"password_required_error": "",
|
||||
"title": "Compartición protegida"
|
||||
},
|
||||
"share_not_found": "Compartición no encontrada",
|
||||
@@ -215,24 +215,41 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"album_cover": null,
|
||||
"cover_photo": null,
|
||||
"reset_cover": null,
|
||||
"set_cover": null,
|
||||
"title_placeholder": null
|
||||
"album_cover": "",
|
||||
"download": {
|
||||
"high-resolutions": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"originals": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"thumbnails": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"web-videos": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"reset_cover": "",
|
||||
"set_cover": "",
|
||||
"title_placeholder": ""
|
||||
},
|
||||
"download": {
|
||||
"filesize": {
|
||||
"byte": "{{count}} Byte",
|
||||
"byte_plural": "{{count}} Bytes",
|
||||
"giga_byte": "{{count}} GB",
|
||||
"giga_byte_plural": null,
|
||||
"giga_byte_plural": "",
|
||||
"kilo_byte": "{{count}} KB",
|
||||
"kilo_byte_plural": null,
|
||||
"kilo_byte_plural": "",
|
||||
"mega_byte": "{{count}} MB",
|
||||
"mega_byte_plural": null,
|
||||
"mega_byte_plural": "",
|
||||
"tera_byte": "{{count}} TB",
|
||||
"tera_byte_plural": null
|
||||
"tera_byte_plural": ""
|
||||
},
|
||||
"table_columns": {
|
||||
"dimensions": "Dimensiones",
|
||||
@@ -285,8 +302,8 @@
|
||||
"sharing": {
|
||||
"add_share": "Añadir compartido",
|
||||
"copy_link": "Copiar enlace",
|
||||
"delete": null,
|
||||
"more": null,
|
||||
"delete": "",
|
||||
"more": "",
|
||||
"no_shares_found": "No se encontraron compartidos",
|
||||
"public_link": "Enlace público",
|
||||
"title": "Opciones de compartir"
|
||||
@@ -301,13 +318,13 @@
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
"dropdown_all": "",
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"loading_album": "Cargando álbum",
|
||||
"login": null,
|
||||
"login": "",
|
||||
"people": "Personas",
|
||||
"settings": "Opciones"
|
||||
}
|
||||
|
||||
@@ -102,7 +102,11 @@
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"title": "opciones de álbum",
|
||||
"title_placeholder": null
|
||||
"title_placeholder": null,
|
||||
"album_cover": null,
|
||||
"cover_photo": "",
|
||||
"reset_cover": null,
|
||||
"set_cover": null
|
||||
},
|
||||
"sharing": {
|
||||
"table_header": "Compartidos públicos",
|
||||
@@ -128,5 +132,11 @@
|
||||
"protected_share": {
|
||||
"password_required_error": null
|
||||
}
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,10 +215,27 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"album_cover": null,
|
||||
"cover_photo": null,
|
||||
"reset_cover": null,
|
||||
"set_cover": null,
|
||||
"album_cover": "",
|
||||
"download": {
|
||||
"high-resolutions": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"originals": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"thumbnails": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"web-videos": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"reset_cover": "",
|
||||
"set_cover": "",
|
||||
"title_placeholder": "Titre de l'Album"
|
||||
},
|
||||
"download": {
|
||||
@@ -301,8 +318,8 @@
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
"dropdown_all": "",
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
|
||||
@@ -87,7 +87,11 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"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": {
|
||||
"table_header": "Partages publics"
|
||||
@@ -95,5 +99,11 @@
|
||||
},
|
||||
"title": {
|
||||
"login": null
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"album_filter": {
|
||||
"only_favorites": "Mostra solo i preferiti",
|
||||
"sort": null,
|
||||
"sort": "",
|
||||
"sorting_options": {
|
||||
"date_imported": "Data importazione",
|
||||
"date_shot": "Data scatto",
|
||||
@@ -35,7 +35,7 @@
|
||||
"placeholder": "Cerca",
|
||||
"result_type": {
|
||||
"albums": "Album",
|
||||
"media": null
|
||||
"media": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -62,10 +62,10 @@
|
||||
},
|
||||
"people_page": {
|
||||
"action_label": {
|
||||
"change_label": null,
|
||||
"detach_face": null,
|
||||
"merge_face": null,
|
||||
"move_faces": null
|
||||
"change_label": "",
|
||||
"detach_face": "",
|
||||
"merge_face": "",
|
||||
"move_faces": ""
|
||||
},
|
||||
"face_group": {
|
||||
"label_placeholder": "Etichetta",
|
||||
@@ -106,10 +106,10 @@
|
||||
},
|
||||
"recognize_unlabeled_faces_button": "Identifica facce senza etichetta",
|
||||
"tableselect_face_group": {
|
||||
"search_faces_placeholder": null
|
||||
"search_faces_placeholder": ""
|
||||
},
|
||||
"tableselect_image_faces": {
|
||||
"search_images_placeholder": null
|
||||
"search_images_placeholder": ""
|
||||
}
|
||||
},
|
||||
"photos_page": {
|
||||
@@ -178,7 +178,7 @@
|
||||
"table": {
|
||||
"column_names": {
|
||||
"action": "Azioni",
|
||||
"capabilities": null,
|
||||
"capabilities": "",
|
||||
"photo_path": "Percorso foto",
|
||||
"username": "Username"
|
||||
},
|
||||
@@ -206,7 +206,7 @@
|
||||
},
|
||||
"protected_share": {
|
||||
"description": "Questa condivisione è protetta da una password.",
|
||||
"password_required_error": null,
|
||||
"password_required_error": "",
|
||||
"title": "Condivisione protetta"
|
||||
},
|
||||
"share_not_found": "Condivisone non trovata",
|
||||
@@ -215,24 +215,41 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"album_cover": null,
|
||||
"cover_photo": null,
|
||||
"reset_cover": null,
|
||||
"set_cover": null,
|
||||
"title_placeholder": null
|
||||
"album_cover": "",
|
||||
"download": {
|
||||
"high-resolutions": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"originals": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"thumbnails": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"web-videos": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"reset_cover": "",
|
||||
"set_cover": "",
|
||||
"title_placeholder": ""
|
||||
},
|
||||
"download": {
|
||||
"filesize": {
|
||||
"byte": "{{count}} Byte",
|
||||
"byte_plural": "{{count}} Bytes",
|
||||
"giga_byte": "{{count}} GB",
|
||||
"giga_byte_plural": null,
|
||||
"giga_byte_plural": "",
|
||||
"kilo_byte": "{{count}} KB",
|
||||
"kilo_byte_plural": null,
|
||||
"kilo_byte_plural": "",
|
||||
"mega_byte": "{{count}} MB",
|
||||
"mega_byte_plural": null,
|
||||
"mega_byte_plural": "",
|
||||
"tera_byte": "{{count}} TB",
|
||||
"tera_byte_plural": null
|
||||
"tera_byte_plural": ""
|
||||
},
|
||||
"table_columns": {
|
||||
"dimensions": "Dimensioni",
|
||||
@@ -285,8 +302,8 @@
|
||||
"sharing": {
|
||||
"add_share": "Aggiungi condivisione",
|
||||
"copy_link": "Copia il link",
|
||||
"delete": null,
|
||||
"more": null,
|
||||
"delete": "",
|
||||
"more": "",
|
||||
"no_shares_found": "Nessuna condivisione trovata",
|
||||
"public_link": "Link pubblico",
|
||||
"title": "Opzioni di condivisione"
|
||||
@@ -301,8 +318,8 @@
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
"dropdown_all": "",
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
|
||||
@@ -64,7 +64,11 @@
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"title": "Opzioni Album",
|
||||
"title_placeholder": null
|
||||
"title_placeholder": null,
|
||||
"album_cover": null,
|
||||
"cover_photo": "",
|
||||
"reset_cover": null,
|
||||
"set_cover": null
|
||||
},
|
||||
"sharing": {
|
||||
"table_header": "Condivisioni pubbliche",
|
||||
@@ -87,5 +91,11 @@
|
||||
"protected_share": {
|
||||
"password_required_error": null
|
||||
}
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"album_filter": {
|
||||
"only_favorites": "Pokaż tylko ulubione",
|
||||
"sort": null,
|
||||
"sort": "",
|
||||
"sorting_options": {
|
||||
"date_imported": "Data zaimportowania",
|
||||
"date_shot": "Data wykonania",
|
||||
@@ -35,7 +35,7 @@
|
||||
"placeholder": "Szukaj",
|
||||
"result_type": {
|
||||
"albums": "Albumy",
|
||||
"media": null
|
||||
"media": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -62,54 +62,54 @@
|
||||
},
|
||||
"people_page": {
|
||||
"action_label": {
|
||||
"change_label": null,
|
||||
"detach_face": null,
|
||||
"merge_face": null,
|
||||
"move_faces": null
|
||||
"change_label": "",
|
||||
"detach_face": "",
|
||||
"merge_face": "",
|
||||
"move_faces": ""
|
||||
},
|
||||
"face_group": {
|
||||
"label_placeholder": "Etykieta",
|
||||
"unlabeled": "Nieoznakowany",
|
||||
"unlabeled_person": null
|
||||
"unlabeled_person": ""
|
||||
},
|
||||
"modal": {
|
||||
"action": {
|
||||
"merge": null
|
||||
"merge": ""
|
||||
},
|
||||
"detach_image_faces": {
|
||||
"action": {
|
||||
"detach": null,
|
||||
"select_images": null
|
||||
"detach": "",
|
||||
"select_images": ""
|
||||
},
|
||||
"description": null,
|
||||
"title": null
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"merge_face_groups": {
|
||||
"description": null,
|
||||
"description": "",
|
||||
"destination_table": {
|
||||
"title": null
|
||||
"title": ""
|
||||
},
|
||||
"title": null
|
||||
"title": ""
|
||||
},
|
||||
"move_image_faces": {
|
||||
"description": null,
|
||||
"description": "",
|
||||
"destination_face_group_table": {
|
||||
"move_action": null,
|
||||
"title": null
|
||||
"move_action": "",
|
||||
"title": ""
|
||||
},
|
||||
"image_select_table": {
|
||||
"next_action": null,
|
||||
"title": null
|
||||
"next_action": "",
|
||||
"title": ""
|
||||
},
|
||||
"title": null
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"recognize_unlabeled_faces_button": "Rozpoznaj nieoznakowane twarze",
|
||||
"tableselect_face_group": {
|
||||
"search_faces_placeholder": null
|
||||
"search_faces_placeholder": ""
|
||||
},
|
||||
"tableselect_image_faces": {
|
||||
"search_images_placeholder": null
|
||||
"search_images_placeholder": ""
|
||||
}
|
||||
},
|
||||
"photos_page": {
|
||||
@@ -178,7 +178,7 @@
|
||||
"table": {
|
||||
"column_names": {
|
||||
"action": "Akcja",
|
||||
"capabilities": null,
|
||||
"capabilities": "",
|
||||
"photo_path": "Ścieżka zdjęć",
|
||||
"username": "Nazwa użytkownika"
|
||||
},
|
||||
@@ -195,9 +195,9 @@
|
||||
"title": "Użytkownicy"
|
||||
},
|
||||
"version_info": {
|
||||
"build_date_title": null,
|
||||
"title": null,
|
||||
"version_title": null
|
||||
"build_date_title": "",
|
||||
"title": "",
|
||||
"version_title": ""
|
||||
}
|
||||
},
|
||||
"share_page": {
|
||||
@@ -206,7 +206,7 @@
|
||||
},
|
||||
"protected_share": {
|
||||
"description": "Ten udział jest chroniony hasłem.",
|
||||
"password_required_error": null,
|
||||
"password_required_error": "",
|
||||
"title": "Udział chroniony"
|
||||
},
|
||||
"share_not_found": "Nie znaleziono udziału",
|
||||
@@ -215,29 +215,46 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"album_cover": null,
|
||||
"cover_photo": null,
|
||||
"reset_cover": null,
|
||||
"set_cover": null,
|
||||
"title_placeholder": null
|
||||
"album_cover": "",
|
||||
"download": {
|
||||
"high-resolutions": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"originals": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"thumbnails": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"web-videos": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"reset_cover": "",
|
||||
"set_cover": "",
|
||||
"title_placeholder": ""
|
||||
},
|
||||
"download": {
|
||||
"filesize": {
|
||||
"byte_0": null,
|
||||
"byte_1": null,
|
||||
"byte_2": null,
|
||||
"giga_byte_0": null,
|
||||
"giga_byte_1": null,
|
||||
"giga_byte_2": null,
|
||||
"kilo_byte_0": null,
|
||||
"kilo_byte_1": null,
|
||||
"kilo_byte_2": null,
|
||||
"mega_byte_0": null,
|
||||
"mega_byte_1": null,
|
||||
"mega_byte_2": null,
|
||||
"tera_byte_0": null,
|
||||
"tera_byte_1": null,
|
||||
"tera_byte_2": null
|
||||
"byte_0": "",
|
||||
"byte_1": "",
|
||||
"byte_2": "",
|
||||
"giga_byte_0": "",
|
||||
"giga_byte_1": "",
|
||||
"giga_byte_2": "",
|
||||
"kilo_byte_0": "",
|
||||
"kilo_byte_1": "",
|
||||
"kilo_byte_2": "",
|
||||
"mega_byte_0": "",
|
||||
"mega_byte_1": "",
|
||||
"mega_byte_2": "",
|
||||
"tera_byte_0": "",
|
||||
"tera_byte_1": "",
|
||||
"tera_byte_2": ""
|
||||
},
|
||||
"table_columns": {
|
||||
"dimensions": "Wymiary",
|
||||
@@ -290,8 +307,8 @@
|
||||
"sharing": {
|
||||
"add_share": "Dodaj udział",
|
||||
"copy_link": "Skopiuj link",
|
||||
"delete": null,
|
||||
"more": null,
|
||||
"delete": "",
|
||||
"more": "",
|
||||
"no_shares_found": "Nie znaleziono udostępnionych",
|
||||
"public_link": "Link publiczny",
|
||||
"title": "Opcje udostępniania"
|
||||
@@ -306,13 +323,13 @@
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
"dropdown_all": "",
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"loading_album": "Ładowanie albumu",
|
||||
"login": null,
|
||||
"login": "",
|
||||
"people": "Ludzie",
|
||||
"settings": "Ustawienia"
|
||||
}
|
||||
|
||||
@@ -102,7 +102,11 @@
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"title": "Opcje albumu",
|
||||
"title_placeholder": null
|
||||
"title_placeholder": null,
|
||||
"album_cover": null,
|
||||
"cover_photo": "",
|
||||
"reset_cover": null,
|
||||
"set_cover": null
|
||||
},
|
||||
"download": {
|
||||
"filesize": {
|
||||
@@ -145,5 +149,11 @@
|
||||
"protected_share": {
|
||||
"password_required_error": null
|
||||
}
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"album_filter": {
|
||||
"only_favorites": "Показать только избранные",
|
||||
"sort": null,
|
||||
"sort": "",
|
||||
"sorting_options": {
|
||||
"date_imported": "Дата импортирования",
|
||||
"date_shot": "Дата снимка",
|
||||
@@ -35,7 +35,7 @@
|
||||
"placeholder": "Поиск",
|
||||
"result_type": {
|
||||
"albums": "Альбомы",
|
||||
"media": null
|
||||
"media": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -62,10 +62,10 @@
|
||||
},
|
||||
"people_page": {
|
||||
"action_label": {
|
||||
"change_label": null,
|
||||
"detach_face": null,
|
||||
"merge_face": null,
|
||||
"move_faces": null
|
||||
"change_label": "",
|
||||
"detach_face": "",
|
||||
"merge_face": "",
|
||||
"move_faces": ""
|
||||
},
|
||||
"face_group": {
|
||||
"label_placeholder": "Метка",
|
||||
@@ -106,10 +106,10 @@
|
||||
},
|
||||
"recognize_unlabeled_faces_button": "Распознавать непомеченные лица",
|
||||
"tableselect_face_group": {
|
||||
"search_faces_placeholder": null
|
||||
"search_faces_placeholder": ""
|
||||
},
|
||||
"tableselect_image_faces": {
|
||||
"search_images_placeholder": null
|
||||
"search_images_placeholder": ""
|
||||
}
|
||||
},
|
||||
"photos_page": {
|
||||
@@ -178,7 +178,7 @@
|
||||
"table": {
|
||||
"column_names": {
|
||||
"action": "Действие",
|
||||
"capabilities": null,
|
||||
"capabilities": "",
|
||||
"photo_path": "Путь к фото",
|
||||
"username": "Имя пользователя"
|
||||
},
|
||||
@@ -206,7 +206,7 @@
|
||||
},
|
||||
"protected_share": {
|
||||
"description": "Это общее медиа защищено паролем.",
|
||||
"password_required_error": null,
|
||||
"password_required_error": "",
|
||||
"title": "Защищённое медиа"
|
||||
},
|
||||
"share_not_found": "Общее медиа не найдено",
|
||||
@@ -215,29 +215,46 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"album_cover": null,
|
||||
"cover_photo": null,
|
||||
"reset_cover": null,
|
||||
"set_cover": null,
|
||||
"title_placeholder": null
|
||||
"album_cover": "",
|
||||
"download": {
|
||||
"high-resolutions": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"originals": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"thumbnails": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"web-videos": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"reset_cover": "",
|
||||
"set_cover": "",
|
||||
"title_placeholder": ""
|
||||
},
|
||||
"download": {
|
||||
"filesize": {
|
||||
"byte_0": null,
|
||||
"byte_1": null,
|
||||
"byte_2": null,
|
||||
"giga_byte_0": null,
|
||||
"giga_byte_1": null,
|
||||
"giga_byte_2": null,
|
||||
"kilo_byte_0": null,
|
||||
"kilo_byte_1": null,
|
||||
"kilo_byte_2": null,
|
||||
"mega_byte_0": null,
|
||||
"mega_byte_1": null,
|
||||
"mega_byte_2": null,
|
||||
"tera_byte_0": null,
|
||||
"tera_byte_1": null,
|
||||
"tera_byte_2": null
|
||||
"byte_0": "",
|
||||
"byte_1": "",
|
||||
"byte_2": "",
|
||||
"giga_byte_0": "",
|
||||
"giga_byte_1": "",
|
||||
"giga_byte_2": "",
|
||||
"kilo_byte_0": "",
|
||||
"kilo_byte_1": "",
|
||||
"kilo_byte_2": "",
|
||||
"mega_byte_0": "",
|
||||
"mega_byte_1": "",
|
||||
"mega_byte_2": "",
|
||||
"tera_byte_0": "",
|
||||
"tera_byte_1": "",
|
||||
"tera_byte_2": ""
|
||||
},
|
||||
"table_columns": {
|
||||
"dimensions": "Габариты",
|
||||
@@ -290,8 +307,8 @@
|
||||
"sharing": {
|
||||
"add_share": "Поделится",
|
||||
"copy_link": "Скопировать ссылку",
|
||||
"delete": null,
|
||||
"more": null,
|
||||
"delete": "",
|
||||
"more": "",
|
||||
"no_shares_found": "Нет доступа",
|
||||
"public_link": "Общедоступная ссылка",
|
||||
"title": "Настройки доступа"
|
||||
@@ -306,8 +323,8 @@
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
"dropdown_all": "",
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
|
||||
@@ -64,7 +64,11 @@
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"title": "Свойства альбома",
|
||||
"title_placeholder": null
|
||||
"title_placeholder": null,
|
||||
"album_cover": null,
|
||||
"cover_photo": "",
|
||||
"reset_cover": null,
|
||||
"set_cover": null
|
||||
},
|
||||
"download": {
|
||||
"filesize": {
|
||||
@@ -104,5 +108,11 @@
|
||||
"protected_share": {
|
||||
"password_required_error": null
|
||||
}
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"album_filter": {
|
||||
"only_favorites": "Visa endast favoriter",
|
||||
"sort": null,
|
||||
"sort": "",
|
||||
"sorting_options": {
|
||||
"date_imported": "Datum för import",
|
||||
"date_shot": "Datum",
|
||||
@@ -35,7 +35,7 @@
|
||||
"placeholder": "Sök",
|
||||
"result_type": {
|
||||
"albums": "Album",
|
||||
"media": null
|
||||
"media": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -62,61 +62,61 @@
|
||||
},
|
||||
"people_page": {
|
||||
"action_label": {
|
||||
"change_label": null,
|
||||
"detach_face": null,
|
||||
"merge_face": null,
|
||||
"move_faces": null
|
||||
"change_label": "",
|
||||
"detach_face": "",
|
||||
"merge_face": "",
|
||||
"move_faces": ""
|
||||
},
|
||||
"face_group": {
|
||||
"label_placeholder": "Märkning",
|
||||
"unlabeled": "Omärkt",
|
||||
"unlabeled_person": null
|
||||
"unlabeled_person": ""
|
||||
},
|
||||
"modal": {
|
||||
"action": {
|
||||
"merge": null
|
||||
"merge": ""
|
||||
},
|
||||
"detach_image_faces": {
|
||||
"action": {
|
||||
"detach": null,
|
||||
"select_images": null
|
||||
"detach": "",
|
||||
"select_images": ""
|
||||
},
|
||||
"description": null,
|
||||
"title": null
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"merge_face_groups": {
|
||||
"description": null,
|
||||
"description": "",
|
||||
"destination_table": {
|
||||
"title": null
|
||||
"title": ""
|
||||
},
|
||||
"title": null
|
||||
"title": ""
|
||||
},
|
||||
"move_image_faces": {
|
||||
"description": null,
|
||||
"description": "",
|
||||
"destination_face_group_table": {
|
||||
"move_action": null,
|
||||
"title": null
|
||||
"move_action": "",
|
||||
"title": ""
|
||||
},
|
||||
"image_select_table": {
|
||||
"next_action": null,
|
||||
"title": null
|
||||
"next_action": "",
|
||||
"title": ""
|
||||
},
|
||||
"title": null
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"recognize_unlabeled_faces_button": "Känna igen omärkta ansikten",
|
||||
"tableselect_face_group": {
|
||||
"search_faces_placeholder": null
|
||||
"search_faces_placeholder": ""
|
||||
},
|
||||
"tableselect_image_faces": {
|
||||
"search_images_placeholder": null
|
||||
"search_images_placeholder": ""
|
||||
}
|
||||
},
|
||||
"photos_page": {
|
||||
"title": "Bilder"
|
||||
},
|
||||
"places_page": {
|
||||
"title": null
|
||||
"title": ""
|
||||
},
|
||||
"routes": {
|
||||
"page_not_found": "Sidan hittades inte"
|
||||
@@ -178,7 +178,7 @@
|
||||
"table": {
|
||||
"column_names": {
|
||||
"action": "Åtgärd",
|
||||
"capabilities": null,
|
||||
"capabilities": "",
|
||||
"photo_path": "Sökväg till bild",
|
||||
"username": "Användarnamn"
|
||||
},
|
||||
@@ -195,9 +195,9 @@
|
||||
"title": "Användare"
|
||||
},
|
||||
"version_info": {
|
||||
"build_date_title": null,
|
||||
"title": null,
|
||||
"version_title": null
|
||||
"build_date_title": "",
|
||||
"title": "",
|
||||
"version_title": ""
|
||||
}
|
||||
},
|
||||
"share_page": {
|
||||
@@ -206,7 +206,7 @@
|
||||
},
|
||||
"protected_share": {
|
||||
"description": "Denna delning är skyddad med ett lösenord.",
|
||||
"password_required_error": null,
|
||||
"password_required_error": "",
|
||||
"title": "Skyddad delning"
|
||||
},
|
||||
"share_not_found": "Delning hittades inte",
|
||||
@@ -215,24 +215,41 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"album_cover": null,
|
||||
"cover_photo": null,
|
||||
"reset_cover": null,
|
||||
"set_cover": null,
|
||||
"title_placeholder": null
|
||||
"album_cover": "",
|
||||
"download": {
|
||||
"high-resolutions": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"originals": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"thumbnails": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
},
|
||||
"web-videos": {
|
||||
"description": "",
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"reset_cover": "",
|
||||
"set_cover": "",
|
||||
"title_placeholder": ""
|
||||
},
|
||||
"download": {
|
||||
"filesize": {
|
||||
"byte": "{{count}} Byte",
|
||||
"byte_plural": "{{count}} Bytes",
|
||||
"giga_byte": "{{count}} GB",
|
||||
"giga_byte_plural": null,
|
||||
"giga_byte_plural": "",
|
||||
"kilo_byte": "{{count}} KB",
|
||||
"kilo_byte_plural": null,
|
||||
"kilo_byte_plural": "",
|
||||
"mega_byte": "{{count}} MB",
|
||||
"mega_byte_plural": null,
|
||||
"mega_byte_plural": "",
|
||||
"tera_byte": "{{count}} TB",
|
||||
"tera_byte_plural": null
|
||||
"tera_byte_plural": ""
|
||||
},
|
||||
"table_columns": {
|
||||
"dimensions": "Mått",
|
||||
@@ -285,8 +302,8 @@
|
||||
"sharing": {
|
||||
"add_share": "Dela",
|
||||
"copy_link": "Kopiera länk",
|
||||
"delete": null,
|
||||
"more": null,
|
||||
"delete": "",
|
||||
"more": "",
|
||||
"no_shares_found": "Inga delningar hittades",
|
||||
"public_link": "Publika länkar",
|
||||
"title": "Delningsinställningar"
|
||||
@@ -301,13 +318,13 @@
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
"dropdown_all": "",
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"loading_album": "Laddar album",
|
||||
"login": null,
|
||||
"login": "",
|
||||
"people": "Personer",
|
||||
"settings": "Inställningar"
|
||||
}
|
||||
|
||||
@@ -105,7 +105,11 @@
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"title": "Albuminställningar",
|
||||
"title_placeholder": null
|
||||
"title_placeholder": null,
|
||||
"album_cover": null,
|
||||
"cover_photo": "",
|
||||
"reset_cover": null,
|
||||
"set_cover": null
|
||||
},
|
||||
"sharing": {
|
||||
"table_header": "Publika delningar",
|
||||
@@ -131,5 +135,11 @@
|
||||
"protected_share": {
|
||||
"password_required_error": null
|
||||
}
|
||||
},
|
||||
"timeline_filter": {
|
||||
"date": {
|
||||
"dropdown_all": null,
|
||||
"label": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export function setupLocalization(): void {
|
||||
lng: 'en',
|
||||
fallbackLng: 'en',
|
||||
returnNull: false,
|
||||
returnEmptyString: false,
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
|
||||
Reference in New Issue
Block a user