Merge pull request #532 from photoview/album-download

Album download
This commit is contained in:
Viktor Strate Kløvedal
2021-09-26 13:30:35 +02:00
committed by GitHub
35 changed files with 1091 additions and 551 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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)
})
}

View 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
}

View 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
View 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()
})
}

View File

@@ -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 {

View File

@@ -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')

View File

@@ -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,

View File

@@ -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">

View File

@@ -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>
) )
} }

View File

@@ -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} />

View 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

View File

@@ -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

View File

@@ -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": {

View File

@@ -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
}
} }
} }

View File

@@ -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": {

View File

@@ -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
}
} }
} }

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"
} }

View File

@@ -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
}
} }
} }

View File

@@ -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": {

View File

@@ -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
}
} }
} }

View File

@@ -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": {

View File

@@ -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
}
} }
} }

View File

@@ -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"
} }

View File

@@ -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
}
} }
} }

View File

@@ -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": {

View File

@@ -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
}
} }
} }

View File

@@ -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"
} }

View File

@@ -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
}
} }
} }

View File

@@ -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,