Merge pull request #309 from photoview/typescript
Add Typescript for UI
This commit is contained in:
commit
6ad9181887
|
@ -21,6 +21,7 @@ node_modules/
|
||||||
.cache/
|
.cache/
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
|
@ -72,6 +72,16 @@ func migrate_exif_fields_exposure(db *gorm.DB) error {
|
||||||
|
|
||||||
return tx.Model(&exifModel{}).Table("media_exif").Where("exposure LIKE '%/%'").FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
|
return tx.Model(&exifModel{}).Table("media_exif").Where("exposure LIKE '%/%'").FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
|
||||||
for _, result := range results {
|
for _, result := range results {
|
||||||
|
|
||||||
|
if result.Exposure == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if *result.Exposure == "" {
|
||||||
|
result.Exposure = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
frac := strings.Split(*result.Exposure, "/")
|
frac := strings.Split(*result.Exposure, "/")
|
||||||
if len(frac) != 2 {
|
if len(frac) != 2 {
|
||||||
return errors.Errorf("failed to convert exposure value (%s) expected format x/y", frac)
|
return errors.Errorf("failed to convert exposure value (%s) expected format x/y", frac)
|
||||||
|
@ -147,6 +157,16 @@ func migrate_exif_fields_flash(db *gorm.DB) error {
|
||||||
|
|
||||||
return tx.Model(&exifModel{}).Table("media_exif").Where("flash IS NOT NULL").FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
|
return tx.Model(&exifModel{}).Table("media_exif").Where("flash IS NOT NULL").FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
|
||||||
for _, result := range results {
|
for _, result := range results {
|
||||||
|
|
||||||
|
if result.Flash == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if *result.Flash == "" {
|
||||||
|
result.Flash = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for index, name := range flashDescriptions {
|
for index, name := range flashDescriptions {
|
||||||
if *result.Flash == name {
|
if *result.Flash == name {
|
||||||
*result.Flash = fmt.Sprintf("%d", index)
|
*result.Flash = fmt.Sprintf("%d", index)
|
||||||
|
|
|
@ -1715,29 +1715,29 @@ type Mutation {
|
||||||
scanUser(userId: ID!): ScannerResult! @isAdmin
|
scanUser(userId: ID!): ScannerResult! @isAdmin
|
||||||
|
|
||||||
"Generate share token for album"
|
"Generate share token for album"
|
||||||
shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken @isAuthorized
|
shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken! @isAuthorized
|
||||||
"Generate share token for media"
|
"Generate share token for media"
|
||||||
shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken @isAuthorized
|
shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken! @isAuthorized
|
||||||
"Delete a share token by it's token value"
|
"Delete a share token by it's token value"
|
||||||
deleteShareToken(token: String!): ShareToken @isAuthorized
|
deleteShareToken(token: String!): ShareToken! @isAuthorized
|
||||||
"Set a password for a token, if null is passed for the password argument, the password will be cleared"
|
"Set a password for a token, if null is passed for the password argument, the password will be cleared"
|
||||||
protectShareToken(token: String!, password: String): ShareToken @isAuthorized
|
protectShareToken(token: String!, password: String): ShareToken! @isAuthorized
|
||||||
|
|
||||||
"Mark or unmark a media as being a favorite"
|
"Mark or unmark a media as being a favorite"
|
||||||
favoriteMedia(mediaId: ID!, favorite: Boolean!): Media @isAuthorized
|
favoriteMedia(mediaId: ID!, favorite: Boolean!): Media! @isAuthorized
|
||||||
|
|
||||||
updateUser(
|
updateUser(
|
||||||
id: ID!
|
id: ID!
|
||||||
username: String
|
username: String
|
||||||
password: String
|
password: String
|
||||||
admin: Boolean
|
admin: Boolean
|
||||||
): User @isAdmin
|
): User! @isAdmin
|
||||||
createUser(
|
createUser(
|
||||||
username: String!
|
username: String!
|
||||||
password: String
|
password: String
|
||||||
admin: Boolean!
|
admin: Boolean!
|
||||||
): User @isAdmin
|
): User! @isAdmin
|
||||||
deleteUser(id: ID!): User @isAdmin
|
deleteUser(id: ID!): User! @isAdmin
|
||||||
|
|
||||||
"Add a root path from where to look for media for the given user"
|
"Add a root path from where to look for media for the given user"
|
||||||
userAddRootPath(id: ID!, rootPath: String!): Album @isAdmin
|
userAddRootPath(id: ID!, rootPath: String!): Album @isAdmin
|
||||||
|
@ -1842,8 +1842,8 @@ type User {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LanguageTranslation {
|
enum LanguageTranslation {
|
||||||
en,
|
English,
|
||||||
da
|
Danish
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserPreferences {
|
type UserPreferences {
|
||||||
|
@ -1899,8 +1899,8 @@ type MediaDownload {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MediaType {
|
enum MediaType {
|
||||||
photo
|
Photo
|
||||||
video
|
Video
|
||||||
}
|
}
|
||||||
|
|
||||||
type Media {
|
type Media {
|
||||||
|
@ -5084,11 +5084,14 @@ func (ec *executionContext) _Mutation_shareAlbum(ctx context.Context, field grap
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
if resTmp == nil {
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(*models.ShareToken)
|
res := resTmp.(*models.ShareToken)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalOShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res)
|
return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Mutation_shareMedia(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Mutation_shareMedia(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
|
@ -5143,11 +5146,14 @@ func (ec *executionContext) _Mutation_shareMedia(ctx context.Context, field grap
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
if resTmp == nil {
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(*models.ShareToken)
|
res := resTmp.(*models.ShareToken)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalOShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res)
|
return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Mutation_deleteShareToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Mutation_deleteShareToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
|
@ -5202,11 +5208,14 @@ func (ec *executionContext) _Mutation_deleteShareToken(ctx context.Context, fiel
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
if resTmp == nil {
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(*models.ShareToken)
|
res := resTmp.(*models.ShareToken)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalOShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res)
|
return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Mutation_protectShareToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Mutation_protectShareToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
|
@ -5261,11 +5270,14 @@ func (ec *executionContext) _Mutation_protectShareToken(ctx context.Context, fie
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
if resTmp == nil {
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(*models.ShareToken)
|
res := resTmp.(*models.ShareToken)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalOShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res)
|
return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Mutation_favoriteMedia(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Mutation_favoriteMedia(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
|
@ -5320,11 +5332,14 @@ func (ec *executionContext) _Mutation_favoriteMedia(ctx context.Context, field g
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
if resTmp == nil {
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(*models.Media)
|
res := resTmp.(*models.Media)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalOMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, field.Selections, res)
|
return ec.marshalNMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Mutation_updateUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Mutation_updateUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
|
@ -5379,11 +5394,14 @@ func (ec *executionContext) _Mutation_updateUser(ctx context.Context, field grap
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
if resTmp == nil {
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(*models.User)
|
res := resTmp.(*models.User)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalOUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res)
|
return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Mutation_createUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Mutation_createUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
|
@ -5438,11 +5456,14 @@ func (ec *executionContext) _Mutation_createUser(ctx context.Context, field grap
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
if resTmp == nil {
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(*models.User)
|
res := resTmp.(*models.User)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalOUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res)
|
return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Mutation_deleteUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Mutation_deleteUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
|
@ -5497,11 +5518,14 @@ func (ec *executionContext) _Mutation_deleteUser(ctx context.Context, field grap
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
if resTmp == nil {
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(*models.User)
|
res := resTmp.(*models.User)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalOUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res)
|
return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Mutation_userAddRootPath(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Mutation_userAddRootPath(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
|
@ -10520,20 +10544,44 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
|
||||||
}
|
}
|
||||||
case "shareAlbum":
|
case "shareAlbum":
|
||||||
out.Values[i] = ec._Mutation_shareAlbum(ctx, field)
|
out.Values[i] = ec._Mutation_shareAlbum(ctx, field)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
invalids++
|
||||||
|
}
|
||||||
case "shareMedia":
|
case "shareMedia":
|
||||||
out.Values[i] = ec._Mutation_shareMedia(ctx, field)
|
out.Values[i] = ec._Mutation_shareMedia(ctx, field)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
invalids++
|
||||||
|
}
|
||||||
case "deleteShareToken":
|
case "deleteShareToken":
|
||||||
out.Values[i] = ec._Mutation_deleteShareToken(ctx, field)
|
out.Values[i] = ec._Mutation_deleteShareToken(ctx, field)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
invalids++
|
||||||
|
}
|
||||||
case "protectShareToken":
|
case "protectShareToken":
|
||||||
out.Values[i] = ec._Mutation_protectShareToken(ctx, field)
|
out.Values[i] = ec._Mutation_protectShareToken(ctx, field)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
invalids++
|
||||||
|
}
|
||||||
case "favoriteMedia":
|
case "favoriteMedia":
|
||||||
out.Values[i] = ec._Mutation_favoriteMedia(ctx, field)
|
out.Values[i] = ec._Mutation_favoriteMedia(ctx, field)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
invalids++
|
||||||
|
}
|
||||||
case "updateUser":
|
case "updateUser":
|
||||||
out.Values[i] = ec._Mutation_updateUser(ctx, field)
|
out.Values[i] = ec._Mutation_updateUser(ctx, field)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
invalids++
|
||||||
|
}
|
||||||
case "createUser":
|
case "createUser":
|
||||||
out.Values[i] = ec._Mutation_createUser(ctx, field)
|
out.Values[i] = ec._Mutation_createUser(ctx, field)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
invalids++
|
||||||
|
}
|
||||||
case "deleteUser":
|
case "deleteUser":
|
||||||
out.Values[i] = ec._Mutation_deleteUser(ctx, field)
|
out.Values[i] = ec._Mutation_deleteUser(ctx, field)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
invalids++
|
||||||
|
}
|
||||||
case "userAddRootPath":
|
case "userAddRootPath":
|
||||||
out.Values[i] = ec._Mutation_userAddRootPath(ctx, field)
|
out.Values[i] = ec._Mutation_userAddRootPath(ctx, field)
|
||||||
case "userRemoveRootAlbum":
|
case "userRemoveRootAlbum":
|
||||||
|
@ -12685,13 +12733,6 @@ func (ec *executionContext) marshalOTime2ᚖtimeᚐTime(ctx context.Context, sel
|
||||||
return graphql.MarshalTime(*v)
|
return graphql.MarshalTime(*v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) marshalOUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx context.Context, sel ast.SelectionSet, v *models.User) graphql.Marshaler {
|
|
||||||
if v == nil {
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
return ec._User(ctx, sel, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) marshalOVideoMetadata2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐVideoMetadata(ctx context.Context, sel ast.SelectionSet, v *models.VideoMetadata) graphql.Marshaler {
|
func (ec *executionContext) marshalOVideoMetadata2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐVideoMetadata(ctx context.Context, sel ast.SelectionSet, v *models.VideoMetadata) graphql.Marshaler {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
|
|
|
@ -71,18 +71,18 @@ type TimelineGroup struct {
|
||||||
type LanguageTranslation string
|
type LanguageTranslation string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LanguageTranslationEn LanguageTranslation = "en"
|
LanguageTranslationEnglish LanguageTranslation = "English"
|
||||||
LanguageTranslationDa LanguageTranslation = "da"
|
LanguageTranslationDanish LanguageTranslation = "Danish"
|
||||||
)
|
)
|
||||||
|
|
||||||
var AllLanguageTranslation = []LanguageTranslation{
|
var AllLanguageTranslation = []LanguageTranslation{
|
||||||
LanguageTranslationEn,
|
LanguageTranslationEnglish,
|
||||||
LanguageTranslationDa,
|
LanguageTranslationDanish,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e LanguageTranslation) IsValid() bool {
|
func (e LanguageTranslation) IsValid() bool {
|
||||||
switch e {
|
switch e {
|
||||||
case LanguageTranslationEn, LanguageTranslationDa:
|
case LanguageTranslationEnglish, LanguageTranslationDanish:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -112,8 +112,8 @@ func (e LanguageTranslation) MarshalGQL(w io.Writer) {
|
||||||
type MediaType string
|
type MediaType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MediaTypePhoto MediaType = "photo"
|
MediaTypePhoto MediaType = "Photo"
|
||||||
MediaTypeVideo MediaType = "video"
|
MediaTypeVideo MediaType = "Video"
|
||||||
)
|
)
|
||||||
|
|
||||||
var AllMediaType = []MediaType{
|
var AllMediaType = []MediaType{
|
||||||
|
|
|
@ -42,6 +42,29 @@ func (m *Media) BeforeSave(tx *gorm.DB) error {
|
||||||
// Update path hash
|
// Update path hash
|
||||||
m.PathHash = MD5Hash(m.Path)
|
m.PathHash = MD5Hash(m.Path)
|
||||||
|
|
||||||
|
// Save media type as lowercase for better compatibility
|
||||||
|
m.Type = MediaType(strings.ToLower(string(m.Type)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Media) AfterFind(tx *gorm.DB) error {
|
||||||
|
|
||||||
|
// Convert lowercased media type back
|
||||||
|
lowercasedType := strings.ToLower(string(m.Type))
|
||||||
|
foundType := false
|
||||||
|
for _, t := range AllMediaType {
|
||||||
|
if strings.ToLower(string(m.Type)) == lowercasedType {
|
||||||
|
m.Type = t
|
||||||
|
foundType = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundType == false {
|
||||||
|
return errors.New(fmt.Sprintf("Failed to parse media from DB: Invalid media type: %s", m.Type))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,29 +95,29 @@ type Mutation {
|
||||||
scanUser(userId: ID!): ScannerResult! @isAdmin
|
scanUser(userId: ID!): ScannerResult! @isAdmin
|
||||||
|
|
||||||
"Generate share token for album"
|
"Generate share token for album"
|
||||||
shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken @isAuthorized
|
shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken! @isAuthorized
|
||||||
"Generate share token for media"
|
"Generate share token for media"
|
||||||
shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken @isAuthorized
|
shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken! @isAuthorized
|
||||||
"Delete a share token by it's token value"
|
"Delete a share token by it's token value"
|
||||||
deleteShareToken(token: String!): ShareToken @isAuthorized
|
deleteShareToken(token: String!): ShareToken! @isAuthorized
|
||||||
"Set a password for a token, if null is passed for the password argument, the password will be cleared"
|
"Set a password for a token, if null is passed for the password argument, the password will be cleared"
|
||||||
protectShareToken(token: String!, password: String): ShareToken @isAuthorized
|
protectShareToken(token: String!, password: String): ShareToken! @isAuthorized
|
||||||
|
|
||||||
"Mark or unmark a media as being a favorite"
|
"Mark or unmark a media as being a favorite"
|
||||||
favoriteMedia(mediaId: ID!, favorite: Boolean!): Media @isAuthorized
|
favoriteMedia(mediaId: ID!, favorite: Boolean!): Media! @isAuthorized
|
||||||
|
|
||||||
updateUser(
|
updateUser(
|
||||||
id: ID!
|
id: ID!
|
||||||
username: String
|
username: String
|
||||||
password: String
|
password: String
|
||||||
admin: Boolean
|
admin: Boolean
|
||||||
): User @isAdmin
|
): User! @isAdmin
|
||||||
createUser(
|
createUser(
|
||||||
username: String!
|
username: String!
|
||||||
password: String
|
password: String
|
||||||
admin: Boolean!
|
admin: Boolean!
|
||||||
): User @isAdmin
|
): User! @isAdmin
|
||||||
deleteUser(id: ID!): User @isAdmin
|
deleteUser(id: ID!): User! @isAdmin
|
||||||
|
|
||||||
"Add a root path from where to look for media for the given user"
|
"Add a root path from where to look for media for the given user"
|
||||||
userAddRootPath(id: ID!, rootPath: String!): Album @isAdmin
|
userAddRootPath(id: ID!, rootPath: String!): Album @isAdmin
|
||||||
|
@ -222,8 +222,8 @@ type User {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LanguageTranslation {
|
enum LanguageTranslation {
|
||||||
en,
|
English,
|
||||||
da
|
Danish
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserPreferences {
|
type UserPreferences {
|
||||||
|
@ -279,8 +279,8 @@ type MediaDownload {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MediaType {
|
enum MediaType {
|
||||||
photo
|
Photo
|
||||||
video
|
Video
|
||||||
}
|
}
|
||||||
|
|
||||||
type Media {
|
type Media {
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es6: true,
|
es6: true,
|
||||||
},
|
},
|
||||||
extends: ['eslint:recommended', 'plugin:react/recommended'],
|
ignorePatterns: ['node_modules', 'dist'],
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:@typescript-eslint/eslint-recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'prettier',
|
||||||
|
],
|
||||||
globals: {
|
globals: {
|
||||||
Atomics: 'readonly',
|
Atomics: 'readonly',
|
||||||
SharedArrayBuffer: 'readonly',
|
SharedArrayBuffer: 'readonly',
|
||||||
|
@ -18,17 +27,20 @@ module.exports = {
|
||||||
ecmaVersion: 2018,
|
ecmaVersion: 2018,
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
plugins: ['react', 'react-hooks'],
|
plugins: ['react', 'react-hooks', '@typescript-eslint'],
|
||||||
rules: {
|
rules: {
|
||||||
'no-unused-vars': 'warn',
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'warn',
|
||||||
'react/display-name': 'off',
|
'react/display-name': 'off',
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
version: 'detect',
|
version: 'detect',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
parser: 'babel-eslint',
|
// parser: 'babel-eslint',
|
||||||
overrides: [
|
overrides: [
|
||||||
Object.assign(require('eslint-plugin-jest').configs.recommended, {
|
Object.assign(require('eslint-plugin-jest').configs.recommended, {
|
||||||
files: ['**/*.test.js'],
|
files: ['**/*.test.js'],
|
||||||
|
@ -42,5 +54,11 @@ module.exports = {
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
files: ['**/*.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
//==============================================================
|
||||||
|
// START Enums and Input Objects
|
||||||
|
//==============================================================
|
||||||
|
|
||||||
|
export enum LanguageTranslation {
|
||||||
|
Danish = "Danish",
|
||||||
|
English = "English",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MediaType {
|
||||||
|
Photo = "Photo",
|
||||||
|
Video = "Video",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum NotificationType {
|
||||||
|
Close = "Close",
|
||||||
|
Message = "Message",
|
||||||
|
Progress = "Progress",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OrderDirection {
|
||||||
|
ASC = "ASC",
|
||||||
|
DESC = "DESC",
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================
|
||||||
|
// END Enums and Input Objects
|
||||||
|
//==============================================================
|
|
@ -0,0 +1,8 @@
|
||||||
|
module.exports = {
|
||||||
|
client: {
|
||||||
|
service: {
|
||||||
|
name: 'photoview',
|
||||||
|
localSchemaFile: '../api/graphql/schema.graphql',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
|
@ -2,27 +2,25 @@ module.exports = function (api) {
|
||||||
const isTest = api.env('test')
|
const isTest = api.env('test')
|
||||||
const isProduction = api.env('NODE_ENV') == 'production'
|
const isProduction = api.env('NODE_ENV') == 'production'
|
||||||
|
|
||||||
let presets = ['@babel/preset-react']
|
let presets = ['@babel/preset-react', '@babel/preset-typescript']
|
||||||
let plugins = []
|
let plugins = []
|
||||||
|
|
||||||
if (isTest) {
|
if (isTest) {
|
||||||
presets.push('@babel/preset-env')
|
presets.push('@babel/preset-env')
|
||||||
|
|
||||||
plugins.push('@babel/plugin-transform-runtime')
|
plugins.push('@babel/plugin-transform-runtime')
|
||||||
plugins.push('@babel/plugin-transform-modules-commonjs')
|
|
||||||
} else {
|
} else {
|
||||||
plugins.push(['styled-components', { pure: true }])
|
|
||||||
plugins.push('graphql-tag')
|
|
||||||
if (!isProduction) {
|
if (!isProduction) {
|
||||||
plugins.push([
|
plugins.push([
|
||||||
'i18next-extract',
|
'i18next-extract',
|
||||||
{
|
{
|
||||||
locales: ['en', 'da'],
|
locales: ['en', 'da'],
|
||||||
discardOldKeys: true,
|
|
||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plugins.push(['styled-components', { pure: true }])
|
||||||
|
plugins.push('graphql-tag')
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
24
ui/build.mjs
24
ui/build.mjs
|
@ -20,10 +20,10 @@ const defineEnv = ENVIRONMENT_VARIABLES.reduce((acc, key) => {
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
const esbuildOptions = {
|
const esbuildOptions = {
|
||||||
entryPoints: ['src/index.js'],
|
entryPoints: ['src/index.tsx'],
|
||||||
plugins: [
|
plugins: [
|
||||||
babel({
|
babel({
|
||||||
filter: /photoview\/ui\/src\/.*\.js$/,
|
filter: /photoview\/ui\/src\/.*\.(js|tsx?)$/,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
publicPath: process.env.UI_PUBLIC_URL || '/',
|
publicPath: process.env.UI_PUBLIC_URL || '/',
|
||||||
|
@ -66,25 +66,25 @@ if (watchMode) {
|
||||||
open: false,
|
open: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
bs.watch('src/**/*.js').on('change', async args => {
|
bs.watch('src/**/*.@(js|tsx|ts)').on('change', async args => {
|
||||||
console.log('reloading', args)
|
console.log('reloading', args)
|
||||||
builderPromise = (await builderPromise).rebuild()
|
builderPromise = (await builderPromise).rebuild()
|
||||||
bs.reload(args)
|
// bs.reload(args)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const esbuildPromise = esbuild
|
const build = async () => {
|
||||||
.build(esbuildOptions)
|
await esbuild.build(esbuildOptions)
|
||||||
.then(() => console.log('esbuild done'))
|
|
||||||
|
|
||||||
const workboxPromise = workboxBuild
|
console.log('esbuild done')
|
||||||
.generateSW({
|
|
||||||
|
await workboxBuild.generateSW({
|
||||||
globDirectory: 'dist/',
|
globDirectory: 'dist/',
|
||||||
globPatterns: ['**/*.{png,svg,woff2,ttf,eot,woff,js,ico,html,json,css}'],
|
globPatterns: ['**/*.{png,svg,woff2,ttf,eot,woff,js,ico,html,json,css}'],
|
||||||
swDest: 'dist/service-worker.js',
|
swDest: 'dist/service-worker.js',
|
||||||
})
|
})
|
||||||
.then(() => console.log('workbox done'))
|
|
||||||
|
|
||||||
Promise.all([esbuildPromise, workboxPromise]).then(() =>
|
console.log('workbox done')
|
||||||
console.log('build complete')
|
console.log('build complete')
|
||||||
)
|
}
|
||||||
|
build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,9 @@
|
||||||
},
|
},
|
||||||
"welcome": "Velkommen til Photoview"
|
"welcome": "Velkommen til Photoview"
|
||||||
},
|
},
|
||||||
|
"meta": {
|
||||||
|
"description": "Simpelt og Brugervenligt Photo-galleri for Personlige Servere"
|
||||||
|
},
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"face_group": {
|
"face_group": {
|
||||||
"label_placeholder": "Navn",
|
"label_placeholder": "Navn",
|
||||||
|
@ -69,6 +72,9 @@
|
||||||
},
|
},
|
||||||
"recognize_unlabeled_faces_button": "Genkend ikke navngivede ansigter"
|
"recognize_unlabeled_faces_button": "Genkend ikke navngivede ansigter"
|
||||||
},
|
},
|
||||||
|
"photos_page": {
|
||||||
|
"title": "Billeder"
|
||||||
|
},
|
||||||
"routes": {
|
"routes": {
|
||||||
"page_not_found": "Side ikke fundet"
|
"page_not_found": "Side ikke fundet"
|
||||||
},
|
},
|
||||||
|
@ -77,7 +83,7 @@
|
||||||
"description": "Det maksimale antal medier som må skannes samtidig",
|
"description": "Det maksimale antal medier som må skannes samtidig",
|
||||||
"title": "Samtidige scanner-arbejdere"
|
"title": "Samtidige scanner-arbejdere"
|
||||||
},
|
},
|
||||||
"logout": null,
|
"logout": "Log ud",
|
||||||
"periodic_scanner": {
|
"periodic_scanner": {
|
||||||
"checkbox_label": "Aktiver periodiske scanner",
|
"checkbox_label": "Aktiver periodiske scanner",
|
||||||
"field": {
|
"field": {
|
||||||
|
@ -99,6 +105,10 @@
|
||||||
"title": "Scanner"
|
"title": "Scanner"
|
||||||
},
|
},
|
||||||
"user_preferences": {
|
"user_preferences": {
|
||||||
|
"change_language": {
|
||||||
|
"description": "Set sidens sprog specifikt for denne bruger",
|
||||||
|
"label": "Sprog"
|
||||||
|
},
|
||||||
"language_selector": {
|
"language_selector": {
|
||||||
"placeholder": "Vælg sprog"
|
"placeholder": "Vælg sprog"
|
||||||
},
|
},
|
||||||
|
@ -143,6 +153,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"share_page": {
|
"share_page": {
|
||||||
|
"media": {
|
||||||
|
"title": "Delt medie"
|
||||||
|
},
|
||||||
"protected_share": {
|
"protected_share": {
|
||||||
"description": "Denne deling er låst med en adgangskode.",
|
"description": "Denne deling er låst med en adgangskode.",
|
||||||
"title": "Beskyttet deling"
|
"title": "Beskyttet deling"
|
||||||
|
@ -205,7 +218,7 @@
|
||||||
"exposure": "Lukketid",
|
"exposure": "Lukketid",
|
||||||
"exposure_program": "Lukketid program",
|
"exposure_program": "Lukketid program",
|
||||||
"flash": "Blitz",
|
"flash": "Blitz",
|
||||||
"focal_length": "Fokallængde",
|
"focal_length": "Brændvidde",
|
||||||
"iso": "ISO",
|
"iso": "ISO",
|
||||||
"lens": "Lense",
|
"lens": "Lense",
|
||||||
"maker": "Mærke"
|
"maker": "Mærke"
|
||||||
|
|
|
@ -62,6 +62,9 @@
|
||||||
},
|
},
|
||||||
"welcome": "Welcome to Photoview"
|
"welcome": "Welcome to Photoview"
|
||||||
},
|
},
|
||||||
|
"meta": {
|
||||||
|
"description": "Simple and User-friendly Photo Gallery for Personal Servers"
|
||||||
|
},
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"face_group": {
|
"face_group": {
|
||||||
"label_placeholder": "Label",
|
"label_placeholder": "Label",
|
||||||
|
@ -69,6 +72,9 @@
|
||||||
},
|
},
|
||||||
"recognize_unlabeled_faces_button": "Recognize unlabeled faces"
|
"recognize_unlabeled_faces_button": "Recognize unlabeled faces"
|
||||||
},
|
},
|
||||||
|
"photos_page": {
|
||||||
|
"title": "Photos"
|
||||||
|
},
|
||||||
"routes": {
|
"routes": {
|
||||||
"page_not_found": "Page not found"
|
"page_not_found": "Page not found"
|
||||||
},
|
},
|
||||||
|
@ -99,6 +105,10 @@
|
||||||
"title": "Scanner"
|
"title": "Scanner"
|
||||||
},
|
},
|
||||||
"user_preferences": {
|
"user_preferences": {
|
||||||
|
"change_language": {
|
||||||
|
"description": "Change website language specific for this user",
|
||||||
|
"label": "Website language"
|
||||||
|
},
|
||||||
"language_selector": {
|
"language_selector": {
|
||||||
"placeholder": "Select language"
|
"placeholder": "Select language"
|
||||||
},
|
},
|
||||||
|
@ -143,6 +153,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"share_page": {
|
"share_page": {
|
||||||
|
"media": {
|
||||||
|
"title": "Shared media"
|
||||||
|
},
|
||||||
"protected_share": {
|
"protected_share": {
|
||||||
"description": "This share is protected with a password.",
|
"description": "This share is protected with a password.",
|
||||||
"title": "Protected share"
|
"title": "Protected share"
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,12 +9,10 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"description": "UI app for Photoview",
|
"description": "UI app for Photoview",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.3.13",
|
"@apollo/client": "^3.3.14",
|
||||||
"@babel/core": "^7.13.14",
|
"@babel/core": "^7.13.15",
|
||||||
"@babel/plugin-transform-modules-commonjs": "^7.13.8",
|
|
||||||
"@babel/plugin-transform-runtime": "^7.13.10",
|
|
||||||
"@babel/preset-env": "^7.13.12",
|
|
||||||
"@babel/preset-react": "^7.13.13",
|
"@babel/preset-react": "^7.13.13",
|
||||||
|
"@babel/preset-typescript": "^7.13.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^26.6.3",
|
"babel-jest": "^26.6.3",
|
||||||
"babel-plugin-graphql-tag": "^3.2.0",
|
"babel-plugin-graphql-tag": "^3.2.0",
|
||||||
|
@ -27,14 +25,13 @@
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"esbuild": "^0.8.52",
|
"esbuild": "^0.8.52",
|
||||||
"esbuild-plugin-babel": "^0.2.3",
|
"esbuild-plugin-babel": "^0.2.3",
|
||||||
"eslint": "^7.23.0",
|
"eslint": "^7.24.0",
|
||||||
"eslint-plugin-jest": "^24.3.3",
|
"eslint-plugin-jest": "^24.3.5",
|
||||||
"eslint-plugin-jest-dom": "^3.7.0",
|
"eslint-plugin-jest-dom": "^3.8.0",
|
||||||
"eslint-plugin-react": "^7.23.1",
|
"eslint-plugin-react": "^7.23.2",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"fs-extra": "^9.1.0",
|
"fs-extra": "^9.1.0",
|
||||||
"graphql": "^15.5.0",
|
"i18next": "^20.2.1",
|
||||||
"i18next": "^20.1.0",
|
|
||||||
"mapbox-gl": "^2.2.0",
|
"mapbox-gl": "^2.2.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
@ -49,6 +46,7 @@
|
||||||
"semantic-ui-react": "^2.0.3",
|
"semantic-ui-react": "^2.0.3",
|
||||||
"styled-components": "^5.2.3",
|
"styled-components": "^5.2.3",
|
||||||
"subscriptions-transport-ws": "^0.9.18",
|
"subscriptions-transport-ws": "^0.9.18",
|
||||||
|
"typescript": "^4.2.4",
|
||||||
"url-join": "^4.0.1",
|
"url-join": "^4.0.1",
|
||||||
"workbox-build": "^6.1.2"
|
"workbox-build": "^6.1.2"
|
||||||
},
|
},
|
||||||
|
@ -56,18 +54,33 @@
|
||||||
"start": "node --experimental-modules build.mjs watch",
|
"start": "node --experimental-modules build.mjs watch",
|
||||||
"build": "NODE_ENV=production node --experimental-modules build.mjs",
|
"build": "NODE_ENV=production node --experimental-modules build.mjs",
|
||||||
"test": "npm run lint && npm run jest",
|
"test": "npm run lint && npm run jest",
|
||||||
"lint": "eslint ./src --max-warnings 0 --cache",
|
"lint": "npm run lint:types & npm run lint:eslint",
|
||||||
|
"lint:eslint": "eslint ./src --max-warnings 0 --cache --config .eslintrc.js",
|
||||||
|
"lint:types": "tsc --noemit",
|
||||||
"jest": "jest",
|
"jest": "jest",
|
||||||
|
"genSchemaTypes": "npx apollo client:codegen --target=typescript",
|
||||||
"prepare": "(cd .. && npx husky install)"
|
"prepare": "(cd .. && npx husky install)"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/plugin-transform-runtime": "^7.13.15",
|
||||||
|
"@babel/preset-env": "^7.13.15",
|
||||||
"@testing-library/jest-dom": "^5.11.10",
|
"@testing-library/jest-dom": "^5.11.10",
|
||||||
"@testing-library/react": "^11.2.6",
|
"@testing-library/react": "^11.2.6",
|
||||||
|
"@types/jest": "^26.0.22",
|
||||||
"@types/react": "^17.0.3",
|
"@types/react": "^17.0.3",
|
||||||
|
"@types/react-dom": "^17.0.3",
|
||||||
|
"@types/react-helmet": "^6.1.1",
|
||||||
|
"@types/react-router-dom": "^5.1.7",
|
||||||
|
"@types/styled-components": "^5.1.9",
|
||||||
|
"@types/url-join": "^4.0.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.21.0",
|
||||||
|
"@typescript-eslint/parser": "^4.21.0",
|
||||||
|
"eslint-config-prettier": "^8.1.0",
|
||||||
"husky": "^6.0.0",
|
"husky": "^6.0.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"lint-staged": "^10.5.4",
|
"lint-staged": "^10.5.4",
|
||||||
"prettier": "^2.2.1"
|
"prettier": "^2.2.1",
|
||||||
|
"tsc-files": "^1.1.2"
|
||||||
},
|
},
|
||||||
"cache": {
|
"cache": {
|
||||||
"swDest": "service-worker.js"
|
"swDest": "service-worker.js"
|
||||||
|
@ -84,16 +97,13 @@
|
||||||
"^.+\\.css$"
|
"^.+\\.css$"
|
||||||
],
|
],
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.js$": "babel-jest",
|
"^.+\\.(js|ts|tsx)$": "babel-jest",
|
||||||
"^.+\\.svg$": "<rootDir>/testing/transform-svg.js"
|
"^.+\\.svg$": "<rootDir>/testing/transform-svg.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,json,css,md,graphql}": "prettier --write",
|
"*.{ts,tsx,js,json,css,md,graphql}": "prettier --write",
|
||||||
"*.js": "eslint --cache --fix --max-warnings 0"
|
"*.{js,ts,tsx}": "eslint --cache --fix --max-warnings 0",
|
||||||
},
|
"*.{ts,tsx}": "tsc-files --noEmit"
|
||||||
"sideEffects": [
|
}
|
||||||
"./src/index.js",
|
|
||||||
"./src/localization.js"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
declare module '*.svg' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
|
@ -30,9 +30,12 @@ const GlobalStyle = createGlobalStyle`
|
||||||
`
|
`
|
||||||
|
|
||||||
import 'semantic-ui-css/semantic.min.css'
|
import 'semantic-ui-css/semantic.min.css'
|
||||||
|
import { siteTranslation } from './__generated__/siteTranslation'
|
||||||
|
import { LanguageTranslation } from '../__generated__/globalTypes'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const SITE_TRANSLATION = gql`
|
const SITE_TRANSLATION = gql`
|
||||||
query {
|
query siteTranslation {
|
||||||
myUserPreferences {
|
myUserPreferences {
|
||||||
id
|
id
|
||||||
language
|
language
|
||||||
|
@ -41,7 +44,8 @@ const SITE_TRANSLATION = gql`
|
||||||
`
|
`
|
||||||
|
|
||||||
const loadTranslations = () => {
|
const loadTranslations = () => {
|
||||||
const [loadLang, { data }] = useLazyQuery(SITE_TRANSLATION)
|
console.log('load translation')
|
||||||
|
const [loadLang, { data }] = useLazyQuery<siteTranslation>(SITE_TRANSLATION)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authToken()) {
|
if (authToken()) {
|
||||||
|
@ -50,13 +54,22 @@ const loadTranslations = () => {
|
||||||
}, [authToken()])
|
}, [authToken()])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('loading translations', data)
|
||||||
switch (data?.myUserPreferences.language) {
|
switch (data?.myUserPreferences.language) {
|
||||||
case 'da':
|
case LanguageTranslation.Danish:
|
||||||
import('../extractedTranslations/da/translation.json').then(danish => {
|
import('../extractedTranslations/da/translation.json').then(danish => {
|
||||||
|
console.log('loading danish')
|
||||||
i18n.addResourceBundle('da', 'translation', danish)
|
i18n.addResourceBundle('da', 'translation', danish)
|
||||||
i18n.changeLanguage('da')
|
i18n.changeLanguage('da')
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
case LanguageTranslation.English:
|
||||||
|
import('../extractedTranslations/en/translation.json').then(english => {
|
||||||
|
console.log('loading english')
|
||||||
|
i18n.addResourceBundle('en', 'translation', english)
|
||||||
|
i18n.changeLanguage('en')
|
||||||
|
})
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
i18n.changeLanguage('en')
|
i18n.changeLanguage('en')
|
||||||
}
|
}
|
||||||
|
@ -64,6 +77,7 @@ const loadTranslations = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
loadTranslations()
|
loadTranslations()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -71,7 +85,10 @@ const App = () => {
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Simple and User-friendly Photo Gallery for Personal Servers"
|
content={t(
|
||||||
|
'meta.description',
|
||||||
|
'Simple and User-friendly Photo Gallery for Personal Servers'
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
|
@ -11,7 +11,7 @@ import * as authentication from './helpers/authentication'
|
||||||
|
|
||||||
require('./localization').default()
|
require('./localization').default()
|
||||||
|
|
||||||
jest.mock('./helpers/authentication.js')
|
jest.mock('./helpers/authentication.ts')
|
||||||
|
|
||||||
test('Layout component', async () => {
|
test('Layout component', async () => {
|
||||||
render(
|
render(
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react'
|
import React, { ReactChild } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { NavLink } from 'react-router-dom'
|
import { NavLink } from 'react-router-dom'
|
||||||
|
@ -79,16 +79,28 @@ const SideButtonLink = styled(NavLink)`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const SideButton = props => {
|
type SideButtonProps = {
|
||||||
return (
|
children: ReactChild | ReactChild[]
|
||||||
<SideButtonLink {...props} activeStyle={{ color: '#4183c4' }}>
|
to: string
|
||||||
{props.children}
|
exact: boolean
|
||||||
</SideButtonLink>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SideButton.propTypes = {
|
const SideButton = ({
|
||||||
children: PropTypes.any,
|
children,
|
||||||
|
to,
|
||||||
|
exact,
|
||||||
|
...otherProps
|
||||||
|
}: SideButtonProps) => {
|
||||||
|
return (
|
||||||
|
<SideButtonLink
|
||||||
|
{...otherProps}
|
||||||
|
to={to}
|
||||||
|
exact={exact}
|
||||||
|
activeStyle={{ color: '#4183c4' }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SideButtonLink>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SideButtonLabel = styled.div`
|
const SideButtonLabel = styled.div`
|
||||||
|
@ -130,7 +142,12 @@ export const SideMenu = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Layout = ({ children, title, ...otherProps }) => {
|
type LayoutProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Layout = ({ children, title, ...otherProps }: LayoutProps) => {
|
||||||
return (
|
return (
|
||||||
<Container {...otherProps} data-testid="Layout">
|
<Container {...otherProps} data-testid="Layout">
|
||||||
<Helmet>
|
<Helmet>
|
|
@ -1,16 +1,16 @@
|
||||||
import React, { useCallback, useEffect } from 'react'
|
import React, { useCallback, useEffect } from 'react'
|
||||||
import ReactRouterPropTypes from 'react-router-prop-types'
|
|
||||||
import { useQuery, gql } from '@apollo/client'
|
import { useQuery, gql } from '@apollo/client'
|
||||||
import AlbumGallery from '../../components/albumGallery/AlbumGallery'
|
import AlbumGallery from '../../components/albumGallery/AlbumGallery'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Layout from '../../Layout'
|
import Layout from '../../Layout'
|
||||||
import useURLParameters from '../../hooks/useURLParameters'
|
import useURLParameters, { UrlKeyValuePair } from '../../hooks/useURLParameters'
|
||||||
import useScrollPagination from '../../hooks/useScrollPagination'
|
import useScrollPagination from '../../hooks/useScrollPagination'
|
||||||
import PaginateLoader from '../../components/PaginateLoader'
|
import PaginateLoader from '../../components/PaginateLoader'
|
||||||
import LazyLoad from '../../helpers/LazyLoad'
|
import LazyLoad from '../../helpers/LazyLoad'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { albumQuery, albumQueryVariables } from './__generated__/albumQuery'
|
||||||
|
import { OrderDirection } from '../../../__generated__/globalTypes'
|
||||||
|
|
||||||
const albumQuery = gql`
|
const ALBUM_QUERY = gql`
|
||||||
query albumQuery(
|
query albumQuery(
|
||||||
$id: ID!
|
$id: ID!
|
||||||
$onlyFavorites: Boolean
|
$onlyFavorites: Boolean
|
||||||
|
@ -61,7 +61,16 @@ const albumQuery = gql`
|
||||||
let refetchNeededAll = false
|
let refetchNeededAll = false
|
||||||
let refetchNeededFavorites = false
|
let refetchNeededFavorites = false
|
||||||
|
|
||||||
function AlbumPage({ match }) {
|
type AlbumPageProps = {
|
||||||
|
match: {
|
||||||
|
params: {
|
||||||
|
id: string
|
||||||
|
subPage: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlbumPage({ match }: AlbumPageProps) {
|
||||||
const albumId = match.params.id
|
const albumId = match.params.id
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
@ -69,14 +78,22 @@ function AlbumPage({ match }) {
|
||||||
const { getParam, setParam, setParams } = useURLParameters()
|
const { getParam, setParam, setParams } = useURLParameters()
|
||||||
|
|
||||||
const onlyFavorites = getParam('favorites') == '1' ? true : false
|
const onlyFavorites = getParam('favorites') == '1' ? true : false
|
||||||
const setOnlyFavorites = favorites => setParam('favorites', favorites ? 1 : 0)
|
const setOnlyFavorites = (favorites: boolean) =>
|
||||||
|
setParam('favorites', favorites ? '1' : '0')
|
||||||
|
|
||||||
const orderBy = getParam('orderBy', 'date_shot')
|
const orderBy = getParam('orderBy', 'date_shot')
|
||||||
const orderDirection = getParam('orderDirection', 'ASC')
|
|
||||||
|
|
||||||
const setOrdering = useCallback(
|
const orderDirStr = getParam('orderDirection', 'ASC') || 'hello'
|
||||||
|
const orderDirection = orderDirStr as OrderDirection
|
||||||
|
|
||||||
|
type setOrderingFn = (args: {
|
||||||
|
orderBy?: string
|
||||||
|
orderDirection?: OrderDirection
|
||||||
|
}) => void
|
||||||
|
|
||||||
|
const setOrdering: setOrderingFn = useCallback(
|
||||||
({ orderBy, orderDirection }) => {
|
({ orderBy, orderDirection }) => {
|
||||||
let updatedParams = []
|
const updatedParams: UrlKeyValuePair[] = []
|
||||||
if (orderBy !== undefined) {
|
if (orderBy !== undefined) {
|
||||||
updatedParams.push({ key: 'orderBy', value: orderBy })
|
updatedParams.push({ key: 'orderBy', value: orderBy })
|
||||||
}
|
}
|
||||||
|
@ -89,7 +106,10 @@ function AlbumPage({ match }) {
|
||||||
[setParams]
|
[setParams]
|
||||||
)
|
)
|
||||||
|
|
||||||
const { loading, error, data, refetch, fetchMore } = useQuery(albumQuery, {
|
const { loading, error, data, refetch, fetchMore } = useQuery<
|
||||||
|
albumQuery,
|
||||||
|
albumQueryVariables
|
||||||
|
>(ALBUM_QUERY, {
|
||||||
variables: {
|
variables: {
|
||||||
id: albumId,
|
id: albumId,
|
||||||
onlyFavorites,
|
onlyFavorites,
|
||||||
|
@ -100,7 +120,10 @@ function AlbumPage({ match }) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { containerElem, finished: finishedLoadingMore } = useScrollPagination({
|
const {
|
||||||
|
containerElem,
|
||||||
|
finished: finishedLoadingMore,
|
||||||
|
} = useScrollPagination<albumQuery>({
|
||||||
loading,
|
loading,
|
||||||
fetchMore,
|
fetchMore,
|
||||||
data,
|
data,
|
||||||
|
@ -151,7 +174,6 @@ function AlbumPage({ match }) {
|
||||||
ref={containerElem}
|
ref={containerElem}
|
||||||
album={data && data.album}
|
album={data && data.album}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
showFavoritesToggle
|
|
||||||
setOnlyFavorites={toggleFavorites}
|
setOnlyFavorites={toggleFavorites}
|
||||||
onlyFavorites={onlyFavorites}
|
onlyFavorites={onlyFavorites}
|
||||||
onFavorite={() => (refetchNeededAll = refetchNeededFavorites = true)}
|
onFavorite={() => (refetchNeededAll = refetchNeededFavorites = true)}
|
||||||
|
@ -167,14 +189,4 @@ function AlbumPage({ match }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AlbumPage.propTypes = {
|
|
||||||
...ReactRouterPropTypes,
|
|
||||||
match: PropTypes.shape({
|
|
||||||
params: PropTypes.shape({
|
|
||||||
id: PropTypes.string,
|
|
||||||
subPage: PropTypes.string,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AlbumPage
|
export default AlbumPage
|
|
@ -0,0 +1,118 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { OrderDirection, MediaType } from "./../../../../__generated__/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: albumQuery
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface albumQuery_album_subAlbums_thumbnail_thumbnail {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface albumQuery_album_subAlbums_thumbnail {
|
||||||
|
__typename: "Media";
|
||||||
|
/**
|
||||||
|
* URL to display the media in a smaller resolution
|
||||||
|
*/
|
||||||
|
thumbnail: albumQuery_album_subAlbums_thumbnail_thumbnail | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface albumQuery_album_subAlbums {
|
||||||
|
__typename: "Album";
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* An image in this album used for previewing this album
|
||||||
|
*/
|
||||||
|
thumbnail: albumQuery_album_subAlbums_thumbnail | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface albumQuery_album_media_thumbnail {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number;
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface albumQuery_album_media_highRes {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface albumQuery_album_media_videoWeb {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface albumQuery_album_media {
|
||||||
|
__typename: "Media";
|
||||||
|
id: string;
|
||||||
|
type: MediaType;
|
||||||
|
/**
|
||||||
|
* URL to display the media in a smaller resolution
|
||||||
|
*/
|
||||||
|
thumbnail: albumQuery_album_media_thumbnail | null;
|
||||||
|
/**
|
||||||
|
* URL to display the photo in full resolution, will be null for videos
|
||||||
|
*/
|
||||||
|
highRes: albumQuery_album_media_highRes | null;
|
||||||
|
/**
|
||||||
|
* URL to get the video in a web format that can be played in the browser, will be null for photos
|
||||||
|
*/
|
||||||
|
videoWeb: albumQuery_album_media_videoWeb | null;
|
||||||
|
favorite: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface albumQuery_album {
|
||||||
|
__typename: "Album";
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* The albums contained in this album
|
||||||
|
*/
|
||||||
|
subAlbums: albumQuery_album_subAlbums[];
|
||||||
|
/**
|
||||||
|
* The media inside this album
|
||||||
|
*/
|
||||||
|
media: albumQuery_album_media[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface albumQuery {
|
||||||
|
/**
|
||||||
|
* Get album by id, user must own the album or be admin
|
||||||
|
* If valid tokenCredentials are provided, the album may be retrived without further authentication
|
||||||
|
*/
|
||||||
|
album: albumQuery_album;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface albumQueryVariables {
|
||||||
|
id: string;
|
||||||
|
onlyFavorites?: boolean | null;
|
||||||
|
mediaOrderBy?: string | null;
|
||||||
|
mediaOrderDirection?: OrderDirection | null;
|
||||||
|
limit?: number | null;
|
||||||
|
offset?: number | null;
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: getMyAlbums
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface getMyAlbums_myAlbums_thumbnail_thumbnail {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getMyAlbums_myAlbums_thumbnail {
|
||||||
|
__typename: "Media";
|
||||||
|
/**
|
||||||
|
* URL to display the media in a smaller resolution
|
||||||
|
*/
|
||||||
|
thumbnail: getMyAlbums_myAlbums_thumbnail_thumbnail | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getMyAlbums_myAlbums {
|
||||||
|
__typename: "Album";
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* An image in this album used for previewing this album
|
||||||
|
*/
|
||||||
|
thumbnail: getMyAlbums_myAlbums_thumbnail | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getMyAlbums {
|
||||||
|
/**
|
||||||
|
* List of albums owned by the logged in user.
|
||||||
|
*/
|
||||||
|
myAlbums: getMyAlbums_myAlbums[];
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import { Container } from './loginUtilities'
|
||||||
import { checkInitialSetupQuery, login } from './loginUtilities'
|
import { checkInitialSetupQuery, login } from './loginUtilities'
|
||||||
import { authToken } from '../../helpers/authentication'
|
import { authToken } from '../../helpers/authentication'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { CheckInitialSetup } from './__generated__/CheckInitialSetup'
|
||||||
|
|
||||||
const initialSetupMutation = gql`
|
const initialSetupMutation = gql`
|
||||||
mutation InitialSetup(
|
mutation InitialSetup(
|
||||||
|
@ -35,31 +36,13 @@ const InitialSetupPage = () => {
|
||||||
rootPath: '',
|
rootPath: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleChange = (event, key) => {
|
|
||||||
const value = event.target.value
|
|
||||||
setState(prevState => ({
|
|
||||||
...prevState,
|
|
||||||
[key]: value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const signIn = (event, authorize) => {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
authorize({
|
|
||||||
variables: {
|
|
||||||
username: state.username,
|
|
||||||
password: state.password,
|
|
||||||
rootPath: state.rootPath,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authToken()) {
|
if (authToken()) {
|
||||||
return <Redirect to="/" />
|
return <Redirect to="/" />
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: initialSetupData } = useQuery(checkInitialSetupQuery)
|
const { data: initialSetupData } = useQuery<CheckInitialSetup>(
|
||||||
|
checkInitialSetupQuery
|
||||||
|
)
|
||||||
const initialSetupRedirect = initialSetupData?.siteInfo
|
const initialSetupRedirect = initialSetupData?.siteInfo
|
||||||
?.initialSetup ? null : (
|
?.initialSetup ? null : (
|
||||||
<Redirect to="/" />
|
<Redirect to="/" />
|
||||||
|
@ -78,6 +61,29 @@ const InitialSetupPage = () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
key: string
|
||||||
|
) => {
|
||||||
|
const value = event.target.value
|
||||||
|
setState(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
[key]: value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const signIn = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
authorize({
|
||||||
|
variables: {
|
||||||
|
username: state.username,
|
||||||
|
password: state.password,
|
||||||
|
rootPath: state.rootPath,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let errorMessage = null
|
let errorMessage = null
|
||||||
if (authorizationData && !authorizationData.initialSetupWizard.success) {
|
if (authorizationData && !authorizationData.initialSetupWizard.success) {
|
||||||
errorMessage = authorizationData.initialSetupWizard.status
|
errorMessage = authorizationData.initialSetupWizard.status
|
||||||
|
@ -93,7 +99,7 @@ const InitialSetupPage = () => {
|
||||||
<Form
|
<Form
|
||||||
style={{ width: 500, margin: 'auto' }}
|
style={{ width: 500, margin: 'auto' }}
|
||||||
error={!!errorMessage}
|
error={!!errorMessage}
|
||||||
onSubmit={e => signIn(e, authorize)}
|
onSubmit={signIn}
|
||||||
loading={
|
loading={
|
||||||
authorizeLoading || authorizationData?.initialSetupWizard?.success
|
authorizeLoading || authorizationData?.initialSetupWizard?.success
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react'
|
||||||
import { useQuery, gql, useMutation } from '@apollo/client'
|
import { useQuery, gql, useMutation } from '@apollo/client'
|
||||||
import { Redirect } from 'react-router-dom'
|
import { Redirect } from 'react-router-dom'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { Button, Form, Message, Header } from 'semantic-ui-react'
|
import { Button, Form, Message, Header, HeaderProps } from 'semantic-ui-react'
|
||||||
import { checkInitialSetupQuery, login, Container } from './loginUtilities'
|
import { checkInitialSetupQuery, login, Container } from './loginUtilities'
|
||||||
import { authToken } from '../../helpers/authentication'
|
import { authToken } from '../../helpers/authentication'
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ const StyledLogo = styled.img`
|
||||||
max-height: 128px;
|
max-height: 128px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const LogoHeader = props => {
|
const LogoHeader = (props: HeaderProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
|
@ -0,0 +1,24 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: Authorize
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface Authorize_authorizeUser {
|
||||||
|
__typename: "AuthorizeResult";
|
||||||
|
success: boolean;
|
||||||
|
status: string;
|
||||||
|
token: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Authorize {
|
||||||
|
authorizeUser: Authorize_authorizeUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthorizeVariables {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: CheckInitialSetup
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface CheckInitialSetup_siteInfo {
|
||||||
|
__typename: "SiteInfo";
|
||||||
|
initialSetup: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckInitialSetup {
|
||||||
|
siteInfo: CheckInitialSetup_siteInfo;
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: InitialSetup
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface InitialSetup_initialSetupWizard {
|
||||||
|
__typename: "AuthorizeResult";
|
||||||
|
success: boolean;
|
||||||
|
status: string;
|
||||||
|
token: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitialSetup {
|
||||||
|
/**
|
||||||
|
* Registers the initial user, can only be called if initialSetup from SiteInfo is true
|
||||||
|
*/
|
||||||
|
initialSetupWizard: InitialSetup_initialSetupWizard | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitialSetupVariables {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
rootPath: string;
|
||||||
|
}
|
|
@ -11,9 +11,9 @@ export const checkInitialSetupQuery = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export function login(token) {
|
export function login(token: string) {
|
||||||
saveTokenCookie(token)
|
saveTokenCookie(token)
|
||||||
window.location = '/'
|
window.location.href = '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Container = styled(SemanticContainer)`
|
export const Container = styled(SemanticContainer)`
|
|
@ -39,7 +39,7 @@ export const MY_FACES_QUERY = gql`
|
||||||
`
|
`
|
||||||
|
|
||||||
export const SET_GROUP_LABEL_MUTATION = gql`
|
export const SET_GROUP_LABEL_MUTATION = gql`
|
||||||
mutation($groupID: ID!, $label: String) {
|
mutation setGroupLabel($groupID: ID!, $label: String) {
|
||||||
setFaceGroupLabel(faceGroupID: $groupID, label: $label) {
|
setFaceGroupLabel(faceGroupID: $groupID, label: $label) {
|
||||||
id
|
id
|
||||||
label
|
label
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { MY_FACES_QUERY } from '../PeoplePage'
|
||||||
import SelectFaceGroupTable from './SelectFaceGroupTable'
|
import SelectFaceGroupTable from './SelectFaceGroupTable'
|
||||||
|
|
||||||
const COMBINE_FACES_MUTATION = gql`
|
const COMBINE_FACES_MUTATION = gql`
|
||||||
mutation($destID: ID!, $srcID: ID!) {
|
mutation combineFaces($destID: ID!, $srcID: ID!) {
|
||||||
combineFaceGroups(
|
combineFaceGroups(
|
||||||
destinationFaceGroupID: $destID
|
destinationFaceGroupID: $destID
|
||||||
sourceFaceGroupID: $srcID
|
sourceFaceGroupID: $srcID
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: combineFaces
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface combineFaces_combineFaceGroups {
|
||||||
|
__typename: "FaceGroup";
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface combineFaces {
|
||||||
|
/**
|
||||||
|
* Merge two face groups into a single one, all ImageFaces from source will be moved to destination
|
||||||
|
*/
|
||||||
|
combineFaceGroups: combineFaces_combineFaceGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface combineFacesVariables {
|
||||||
|
destID: string;
|
||||||
|
srcID: string;
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: detachImageFaces
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface detachImageFaces_detachImageFaces {
|
||||||
|
__typename: "FaceGroup";
|
||||||
|
id: string;
|
||||||
|
label: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface detachImageFaces {
|
||||||
|
/**
|
||||||
|
* Move a list of ImageFaces to a new face group
|
||||||
|
*/
|
||||||
|
detachImageFaces: detachImageFaces_detachImageFaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface detachImageFacesVariables {
|
||||||
|
faceIDs: string[];
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: moveImageFaces
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface moveImageFaces_moveImageFaces_imageFaces {
|
||||||
|
__typename: "ImageFace";
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface moveImageFaces_moveImageFaces {
|
||||||
|
__typename: "FaceGroup";
|
||||||
|
id: string;
|
||||||
|
imageFaces: moveImageFaces_moveImageFaces_imageFaces[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface moveImageFaces {
|
||||||
|
/**
|
||||||
|
* Move a list of ImageFaces to another face group
|
||||||
|
*/
|
||||||
|
moveImageFaces: moveImageFaces_moveImageFaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface moveImageFacesVariables {
|
||||||
|
faceIDs: string[];
|
||||||
|
destFaceGroupID: string;
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { MediaType } from "./../../../../../__generated__/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: singleFaceGroup
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface singleFaceGroup_faceGroup_imageFaces_rectangle {
|
||||||
|
__typename: "FaceRectangle";
|
||||||
|
minX: number;
|
||||||
|
maxX: number;
|
||||||
|
minY: number;
|
||||||
|
maxY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface singleFaceGroup_faceGroup_imageFaces_media_thumbnail {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number;
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface singleFaceGroup_faceGroup_imageFaces_media_highRes {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface singleFaceGroup_faceGroup_imageFaces_media {
|
||||||
|
__typename: "Media";
|
||||||
|
id: string;
|
||||||
|
type: MediaType;
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* URL to display the media in a smaller resolution
|
||||||
|
*/
|
||||||
|
thumbnail: singleFaceGroup_faceGroup_imageFaces_media_thumbnail | null;
|
||||||
|
/**
|
||||||
|
* URL to display the photo in full resolution, will be null for videos
|
||||||
|
*/
|
||||||
|
highRes: singleFaceGroup_faceGroup_imageFaces_media_highRes | null;
|
||||||
|
favorite: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface singleFaceGroup_faceGroup_imageFaces {
|
||||||
|
__typename: "ImageFace";
|
||||||
|
id: string;
|
||||||
|
rectangle: singleFaceGroup_faceGroup_imageFaces_rectangle | null;
|
||||||
|
media: singleFaceGroup_faceGroup_imageFaces_media;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface singleFaceGroup_faceGroup {
|
||||||
|
__typename: "FaceGroup";
|
||||||
|
id: string;
|
||||||
|
label: string | null;
|
||||||
|
imageFaces: singleFaceGroup_faceGroup_imageFaces[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface singleFaceGroup {
|
||||||
|
faceGroup: singleFaceGroup_faceGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface singleFaceGroupVariables {
|
||||||
|
id: string;
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: myFaces
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface myFaces_myFaceGroups_imageFaces_rectangle {
|
||||||
|
__typename: "FaceRectangle";
|
||||||
|
minX: number;
|
||||||
|
maxX: number;
|
||||||
|
minY: number;
|
||||||
|
maxY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface myFaces_myFaceGroups_imageFaces_media_thumbnail {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number;
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface myFaces_myFaceGroups_imageFaces_media {
|
||||||
|
__typename: "Media";
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* URL to display the media in a smaller resolution
|
||||||
|
*/
|
||||||
|
thumbnail: myFaces_myFaceGroups_imageFaces_media_thumbnail | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface myFaces_myFaceGroups_imageFaces {
|
||||||
|
__typename: "ImageFace";
|
||||||
|
id: string;
|
||||||
|
rectangle: myFaces_myFaceGroups_imageFaces_rectangle | null;
|
||||||
|
media: myFaces_myFaceGroups_imageFaces_media;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface myFaces_myFaceGroups {
|
||||||
|
__typename: "FaceGroup";
|
||||||
|
id: string;
|
||||||
|
label: string | null;
|
||||||
|
imageFaceCount: number;
|
||||||
|
imageFaces: myFaces_myFaceGroups_imageFaces[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface myFaces {
|
||||||
|
myFaceGroups: myFaces_myFaceGroups[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface myFacesVariables {
|
||||||
|
limit?: number | null;
|
||||||
|
offset?: number | null;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: recognizeUnlabeledFaces
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface recognizeUnlabeledFaces_recognizeUnlabeledFaces {
|
||||||
|
__typename: "ImageFace";
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface recognizeUnlabeledFaces {
|
||||||
|
/**
|
||||||
|
* Check all unlabeled faces to see if they match a labeled FaceGroup, and move them if they match
|
||||||
|
*/
|
||||||
|
recognizeUnlabeledFaces: recognizeUnlabeledFaces_recognizeUnlabeledFaces[];
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: setGroupLabel
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface setGroupLabel_setFaceGroupLabel {
|
||||||
|
__typename: "FaceGroup";
|
||||||
|
id: string;
|
||||||
|
label: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface setGroupLabel {
|
||||||
|
/**
|
||||||
|
* Assign a label to a face group, set label to null to remove the current one
|
||||||
|
*/
|
||||||
|
setFaceGroupLabel: setGroupLabel_setFaceGroupLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface setGroupLabelVariables {
|
||||||
|
groupID: string;
|
||||||
|
label?: string | null;
|
||||||
|
}
|
|
@ -1,24 +1,18 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Layout from '../../Layout'
|
import Layout from '../../Layout'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import TimelineGallery from '../../components/timelineGallery/TimelineGallery'
|
import TimelineGallery from '../../components/timelineGallery/TimelineGallery'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const PhotosPage = () => {
|
const PhotosPage = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Layout title="Photos">
|
<Layout title={t('photos_page.title', 'Photos')}>
|
||||||
<TimelineGallery />
|
<TimelineGallery />
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PhotosPage.propTypes = {
|
|
||||||
match: PropTypes.shape({
|
|
||||||
params: PropTypes.shape({
|
|
||||||
subPage: PropTypes.string,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PhotosPage
|
export default PhotosPage
|
|
@ -0,0 +1,19 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: placePageMapboxToken
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface placePageMapboxToken {
|
||||||
|
/**
|
||||||
|
* Get the mapbox api token, returns null if mapbox is not enabled
|
||||||
|
*/
|
||||||
|
mapboxToken: string | null;
|
||||||
|
/**
|
||||||
|
* Get media owned by the logged in user, returned in GeoJson format
|
||||||
|
*/
|
||||||
|
myMediaGeoJson: any;
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { MediaType } from "./../../../../__generated__/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: placePageQueryMedia
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface placePageQueryMedia_mediaList_thumbnail {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number;
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface placePageQueryMedia_mediaList_highRes {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number;
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface placePageQueryMedia_mediaList_videoWeb {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number;
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface placePageQueryMedia_mediaList {
|
||||||
|
__typename: "Media";
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* URL to display the media in a smaller resolution
|
||||||
|
*/
|
||||||
|
thumbnail: placePageQueryMedia_mediaList_thumbnail | null;
|
||||||
|
/**
|
||||||
|
* URL to display the photo in full resolution, will be null for videos
|
||||||
|
*/
|
||||||
|
highRes: placePageQueryMedia_mediaList_highRes | null;
|
||||||
|
/**
|
||||||
|
* URL to get the video in a web format that can be played in the browser, will be null for photos
|
||||||
|
*/
|
||||||
|
videoWeb: placePageQueryMedia_mediaList_videoWeb | null;
|
||||||
|
type: MediaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface placePageQueryMedia {
|
||||||
|
/**
|
||||||
|
* Get a list of media by their ids, user must own the media or be admin
|
||||||
|
*/
|
||||||
|
mediaList: placePageQueryMedia_mediaList[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface placePageQueryMediaVariables {
|
||||||
|
mediaIDs: string[];
|
||||||
|
}
|
|
@ -4,6 +4,11 @@ import { useMutation, useQuery } from '@apollo/client'
|
||||||
import { Checkbox, Dropdown, Input, Loader } from 'semantic-ui-react'
|
import { Checkbox, Dropdown, Input, Loader } from 'semantic-ui-react'
|
||||||
import { InputLabelDescription, InputLabelTitle } from './SettingsPage'
|
import { InputLabelDescription, InputLabelTitle } from './SettingsPage'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { scanIntervalQuery } from './__generated__/scanIntervalQuery'
|
||||||
|
import {
|
||||||
|
changeScanIntervalMutation,
|
||||||
|
changeScanIntervalMutationVariables,
|
||||||
|
} from './__generated__/changeScanIntervalMutation'
|
||||||
|
|
||||||
const SCAN_INTERVAL_QUERY = gql`
|
const SCAN_INTERVAL_QUERY = gql`
|
||||||
query scanIntervalQuery {
|
query scanIntervalQuery {
|
||||||
|
@ -19,44 +24,57 @@ const SCAN_INTERVAL_MUTATION = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
enum TimeUnit {
|
||||||
|
Second = 'second',
|
||||||
|
Minute = 'minute',
|
||||||
|
Hour = 'hour',
|
||||||
|
Day = 'day',
|
||||||
|
Month = 'month',
|
||||||
|
}
|
||||||
|
|
||||||
const timeUnits = [
|
const timeUnits = [
|
||||||
{
|
{
|
||||||
value: 'second',
|
value: TimeUnit.Second,
|
||||||
multiplier: 1,
|
multiplier: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'minute',
|
value: TimeUnit.Minute,
|
||||||
multiplier: 60,
|
multiplier: 60,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'hour',
|
value: TimeUnit.Hour,
|
||||||
multiplier: 60 * 60,
|
multiplier: 60 * 60,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'day',
|
value: TimeUnit.Day,
|
||||||
multiplier: 60 * 60 * 24,
|
multiplier: 60 * 60 * 24,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'month',
|
value: TimeUnit.Month,
|
||||||
multiplier: 60 * 60 * 24 * 30,
|
multiplier: 60 * 60 * 24 * 30,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const convertToSeconds = ({ value, unit }) => {
|
type TimeValue = {
|
||||||
return parseInt(value * timeUnits.find(x => x.value == unit).multiplier)
|
value: number
|
||||||
|
unit: TimeUnit
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertToAppropriateUnit = ({ value, unit }) => {
|
const convertToSeconds = ({ value, unit }: TimeValue) => {
|
||||||
|
return value * (timeUnits.find(x => x.value == unit)?.multiplier as number)
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertToAppropriateUnit = ({ value, unit }: TimeValue): TimeValue => {
|
||||||
if (value == 0) {
|
if (value == 0) {
|
||||||
return {
|
return {
|
||||||
unit: 'second',
|
unit: TimeUnit.Second,
|
||||||
value: 0,
|
value: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const seconds = convertToSeconds({ value, unit })
|
const seconds = convertToSeconds({ value, unit })
|
||||||
|
|
||||||
let resultingUnit = timeUnits.first
|
let resultingUnit = timeUnits[0]
|
||||||
for (const unit of timeUnits) {
|
for (const unit of timeUnits) {
|
||||||
if (
|
if (
|
||||||
seconds / unit.multiplier >= 1 &&
|
seconds / unit.multiplier >= 1 &&
|
||||||
|
@ -78,26 +96,26 @@ const PeriodicScanner = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const [enablePeriodicScanner, setEnablePeriodicScanner] = useState(false)
|
const [enablePeriodicScanner, setEnablePeriodicScanner] = useState(false)
|
||||||
const [scanInterval, setScanInterval] = useState({
|
const [scanInterval, setScanInterval] = useState<TimeValue>({
|
||||||
value: 4,
|
value: 4,
|
||||||
unit: 'minute',
|
unit: TimeUnit.Minute,
|
||||||
})
|
})
|
||||||
|
|
||||||
const scanIntervalServerValue = useRef(null)
|
const scanIntervalServerValue = useRef<number | null>(null)
|
||||||
|
|
||||||
const scanIntervalQuery = useQuery(SCAN_INTERVAL_QUERY, {
|
const scanIntervalQuery = useQuery<scanIntervalQuery>(SCAN_INTERVAL_QUERY, {
|
||||||
onCompleted(data) {
|
onCompleted(data) {
|
||||||
const queryScanInterval = data.siteInfo.periodicScanInterval
|
const queryScanInterval = data.siteInfo.periodicScanInterval
|
||||||
|
|
||||||
if (queryScanInterval == 0) {
|
if (queryScanInterval == 0) {
|
||||||
setScanInterval({
|
setScanInterval({
|
||||||
unit: 'second',
|
unit: TimeUnit.Second,
|
||||||
value: '',
|
value: 0,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
setScanInterval(
|
setScanInterval(
|
||||||
convertToAppropriateUnit({
|
convertToAppropriateUnit({
|
||||||
unit: 'second',
|
unit: TimeUnit.Second,
|
||||||
value: queryScanInterval,
|
value: queryScanInterval,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -110,15 +128,20 @@ const PeriodicScanner = () => {
|
||||||
const [
|
const [
|
||||||
setScanIntervalMutation,
|
setScanIntervalMutation,
|
||||||
{ loading: scanIntervalMutationLoading },
|
{ loading: scanIntervalMutationLoading },
|
||||||
] = useMutation(SCAN_INTERVAL_MUTATION)
|
] = useMutation<
|
||||||
|
changeScanIntervalMutation,
|
||||||
|
changeScanIntervalMutationVariables
|
||||||
|
>(SCAN_INTERVAL_MUTATION)
|
||||||
|
|
||||||
const onScanIntervalCheckboxChange = checked => {
|
const onScanIntervalCheckboxChange = (checked: boolean) => {
|
||||||
setEnablePeriodicScanner(checked)
|
setEnablePeriodicScanner(checked)
|
||||||
|
|
||||||
onScanIntervalUpdate(checked ? scanInterval : { value: 0, unit: 'second' })
|
onScanIntervalUpdate(
|
||||||
|
checked ? scanInterval : { value: 0, unit: TimeUnit.Second }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onScanIntervalUpdate = scanInterval => {
|
const onScanIntervalUpdate = (scanInterval: TimeValue) => {
|
||||||
const seconds = convertToSeconds(scanInterval)
|
const seconds = convertToSeconds(scanInterval)
|
||||||
|
|
||||||
if (scanIntervalServerValue.current != seconds) {
|
if (scanIntervalServerValue.current != seconds) {
|
||||||
|
@ -131,31 +154,37 @@ const PeriodicScanner = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const scanIntervalUnits = [
|
type scanIntervalUnitType = {
|
||||||
|
key: TimeUnit
|
||||||
|
text: string
|
||||||
|
value: TimeUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
const scanIntervalUnits: scanIntervalUnitType[] = [
|
||||||
{
|
{
|
||||||
key: 'second',
|
key: TimeUnit.Second,
|
||||||
text: t('settings.periodic_scanner.interval_unit.seconds', 'Seconds'),
|
text: t('settings.periodic_scanner.interval_unit.seconds', 'Seconds'),
|
||||||
value: 'second',
|
value: TimeUnit.Second,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'minute',
|
key: TimeUnit.Minute,
|
||||||
text: t('settings.periodic_scanner.interval_unit.minutes', 'Minutes'),
|
text: t('settings.periodic_scanner.interval_unit.minutes', 'Minutes'),
|
||||||
value: 'minute',
|
value: TimeUnit.Minute,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'hour',
|
key: TimeUnit.Hour,
|
||||||
text: t('settings.periodic_scanner.interval_unit.hour', 'Hour'),
|
text: t('settings.periodic_scanner.interval_unit.hour', 'Hour'),
|
||||||
value: 'hour',
|
value: TimeUnit.Hour,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'day',
|
key: TimeUnit.Day,
|
||||||
text: t('settings.periodic_scanner.interval_unit.days', 'Days'),
|
text: t('settings.periodic_scanner.interval_unit.days', 'Days'),
|
||||||
value: 'day',
|
value: TimeUnit.Day,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'month',
|
key: TimeUnit.Month,
|
||||||
text: t('settings.periodic_scanner.interval_unit.months', 'Months'),
|
text: t('settings.periodic_scanner.interval_unit.months', 'Months'),
|
||||||
value: 'month',
|
value: TimeUnit.Month,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -171,7 +200,9 @@ const PeriodicScanner = () => {
|
||||||
)}
|
)}
|
||||||
disabled={scanIntervalQuery.loading}
|
disabled={scanIntervalQuery.loading}
|
||||||
checked={enablePeriodicScanner}
|
checked={enablePeriodicScanner}
|
||||||
onChange={(_, { checked }) => onScanIntervalCheckboxChange(checked)}
|
onChange={(_, { checked }) =>
|
||||||
|
onScanIntervalCheckboxChange(checked || false)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -195,9 +226,9 @@ const PeriodicScanner = () => {
|
||||||
label={
|
label={
|
||||||
<Dropdown
|
<Dropdown
|
||||||
onChange={(_, { value }) => {
|
onChange={(_, { value }) => {
|
||||||
const newScanInterval = {
|
const newScanInterval: TimeValue = {
|
||||||
...scanInterval,
|
...scanInterval,
|
||||||
unit: value,
|
unit: value as TimeUnit,
|
||||||
}
|
}
|
||||||
|
|
||||||
setScanInterval(newScanInterval)
|
setScanInterval(newScanInterval)
|
||||||
|
@ -208,7 +239,7 @@ const PeriodicScanner = () => {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onBlur={() => onScanIntervalUpdate(scanInterval)}
|
onBlur={() => onScanIntervalUpdate(scanInterval)}
|
||||||
onKeyDown={({ key }) =>
|
onKeyDown={({ key }: KeyboardEvent) =>
|
||||||
key == 'Enter' && onScanIntervalUpdate(scanInterval)
|
key == 'Enter' && onScanIntervalUpdate(scanInterval)
|
||||||
}
|
}
|
||||||
loading={scanIntervalQuery.loading}
|
loading={scanIntervalQuery.loading}
|
||||||
|
@ -219,7 +250,7 @@ const PeriodicScanner = () => {
|
||||||
onChange={(_, { value }) => {
|
onChange={(_, { value }) => {
|
||||||
setScanInterval(x => ({
|
setScanInterval(x => ({
|
||||||
...x,
|
...x,
|
||||||
value,
|
value: parseInt(value),
|
||||||
}))
|
}))
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
|
@ -3,6 +3,11 @@ import { useQuery, useMutation, gql } from '@apollo/client'
|
||||||
import { Input, Loader } from 'semantic-ui-react'
|
import { Input, Loader } from 'semantic-ui-react'
|
||||||
import { InputLabelTitle, InputLabelDescription } from './SettingsPage'
|
import { InputLabelTitle, InputLabelDescription } from './SettingsPage'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { concurrentWorkersQuery } from './__generated__/concurrentWorkersQuery'
|
||||||
|
import {
|
||||||
|
setConcurrentWorkers,
|
||||||
|
setConcurrentWorkersVariables,
|
||||||
|
} from './__generated__/setConcurrentWorkers'
|
||||||
|
|
||||||
const CONCURRENT_WORKERS_QUERY = gql`
|
const CONCURRENT_WORKERS_QUERY = gql`
|
||||||
query concurrentWorkersQuery {
|
query concurrentWorkersQuery {
|
||||||
|
@ -21,21 +26,25 @@ const SET_CONCURRENT_WORKERS_MUTATION = gql`
|
||||||
const ScannerConcurrentWorkers = () => {
|
const ScannerConcurrentWorkers = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const workerAmountQuery = useQuery(CONCURRENT_WORKERS_QUERY, {
|
const workerAmountQuery = useQuery<concurrentWorkersQuery>(
|
||||||
onCompleted(data) {
|
CONCURRENT_WORKERS_QUERY,
|
||||||
setWorkerAmount(data.siteInfo.concurrentWorkers)
|
{
|
||||||
workerAmountServerValue.current = data.siteInfo.concurrentWorkers
|
onCompleted(data) {
|
||||||
},
|
setWorkerAmount(data.siteInfo.concurrentWorkers)
|
||||||
})
|
workerAmountServerValue.current = data.siteInfo.concurrentWorkers
|
||||||
|
},
|
||||||
const [setWorkersMutation, workersMutationData] = useMutation(
|
}
|
||||||
SET_CONCURRENT_WORKERS_MUTATION
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const workerAmountServerValue = useRef(null)
|
const [setWorkersMutation, workersMutationData] = useMutation<
|
||||||
const [workerAmount, setWorkerAmount] = useState('')
|
setConcurrentWorkers,
|
||||||
|
setConcurrentWorkersVariables
|
||||||
|
>(SET_CONCURRENT_WORKERS_MUTATION)
|
||||||
|
|
||||||
const updateWorkerAmount = workerAmount => {
|
const workerAmountServerValue = useRef<null | number>(null)
|
||||||
|
const [workerAmount, setWorkerAmount] = useState(0)
|
||||||
|
|
||||||
|
const updateWorkerAmount = (workerAmount: number) => {
|
||||||
if (workerAmountServerValue.current != workerAmount) {
|
if (workerAmountServerValue.current != workerAmount) {
|
||||||
workerAmountServerValue.current = workerAmount
|
workerAmountServerValue.current = workerAmount
|
||||||
setWorkersMutation({
|
setWorkersMutation({
|
||||||
|
@ -67,10 +76,10 @@ const ScannerConcurrentWorkers = () => {
|
||||||
id="scanner_concurrent_workers_field"
|
id="scanner_concurrent_workers_field"
|
||||||
value={workerAmount}
|
value={workerAmount}
|
||||||
onChange={(_, { value }) => {
|
onChange={(_, { value }) => {
|
||||||
setWorkerAmount(value)
|
setWorkerAmount(parseInt(value))
|
||||||
}}
|
}}
|
||||||
onBlur={() => updateWorkerAmount(workerAmount)}
|
onBlur={() => updateWorkerAmount(workerAmount)}
|
||||||
onKeyDown={({ key }) =>
|
onKeyDown={({ key }: KeyboardEvent) =>
|
||||||
key == 'Enter' && updateWorkerAmount(workerAmount)
|
key == 'Enter' && updateWorkerAmount(workerAmount)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
|
@ -5,6 +5,7 @@ import PeriodicScanner from './PeriodicScanner'
|
||||||
import ScannerConcurrentWorkers from './ScannerConcurrentWorkers'
|
import ScannerConcurrentWorkers from './ScannerConcurrentWorkers'
|
||||||
import { SectionTitle, InputLabelDescription } from './SettingsPage'
|
import { SectionTitle, InputLabelDescription } from './SettingsPage'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { scanAllMutation } from './__generated__/scanAllMutation'
|
||||||
|
|
||||||
const SCAN_MUTATION = gql`
|
const SCAN_MUTATION = gql`
|
||||||
mutation scanAllMutation {
|
mutation scanAllMutation {
|
||||||
|
@ -17,7 +18,7 @@ const SCAN_MUTATION = gql`
|
||||||
|
|
||||||
const ScannerSection = () => {
|
const ScannerSection = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [startScanner, { called }] = useMutation(SCAN_MUTATION)
|
const [startScanner, { called }] = useMutation<scanAllMutation>(SCAN_MUTATION)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
|
@ -10,7 +10,7 @@ import ScannerSection from './ScannerSection'
|
||||||
import UserPreferences from './UserPreferences'
|
import UserPreferences from './UserPreferences'
|
||||||
import UsersTable from './Users/UsersTable'
|
import UsersTable from './Users/UsersTable'
|
||||||
|
|
||||||
export const SectionTitle = styled.h2`
|
export const SectionTitle = styled.h2<{ nospace?: boolean }>`
|
||||||
margin-top: ${({ nospace }) => (nospace ? '0' : '1.4em')} !important;
|
margin-top: ${({ nospace }) => (nospace ? '0' : '1.4em')} !important;
|
||||||
padding-bottom: 0.3em;
|
padding-bottom: 0.3em;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
|
@ -1,78 +0,0 @@
|
||||||
import { useMutation, useQuery } from '@apollo/client'
|
|
||||||
import gql from 'graphql-tag'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { Dropdown } from 'semantic-ui-react'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
import { SectionTitle } from './SettingsPage'
|
|
||||||
|
|
||||||
const languagePreferences = [
|
|
||||||
{ key: 1, text: 'English', value: 'en' },
|
|
||||||
{ key: 2, text: 'Dansk', value: 'da' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const CHANGE_USER_PREFERENCES = gql`
|
|
||||||
mutation($language: String) {
|
|
||||||
changeUserPreferences(language: $language) {
|
|
||||||
id
|
|
||||||
language
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const MY_USER_PREFERENCES = gql`
|
|
||||||
query {
|
|
||||||
myUserPreferences {
|
|
||||||
id
|
|
||||||
language
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const UserPreferencesWrapper = styled.div`
|
|
||||||
margin-bottom: 24px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const UserPreferences = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const { data } = useQuery(MY_USER_PREFERENCES)
|
|
||||||
|
|
||||||
const [changePrefs, { loading: loadingPrefs, error }] = useMutation(
|
|
||||||
CHANGE_USER_PREFERENCES
|
|
||||||
)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return error.message
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UserPreferencesWrapper>
|
|
||||||
<SectionTitle nospace>
|
|
||||||
{t('settings.user_preferences.title', 'User preferences')}
|
|
||||||
</SectionTitle>
|
|
||||||
<Dropdown
|
|
||||||
placeholder={t(
|
|
||||||
'settings.user_preferences.language_selector.placeholder',
|
|
||||||
'Select language'
|
|
||||||
)}
|
|
||||||
clearable
|
|
||||||
options={languagePreferences}
|
|
||||||
onChange={(event, { value: language }) => {
|
|
||||||
changePrefs({
|
|
||||||
variables: {
|
|
||||||
language,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
selection
|
|
||||||
value={data?.myUserPreferences.language}
|
|
||||||
loading={loadingPrefs}
|
|
||||||
disabled={loadingPrefs}
|
|
||||||
/>
|
|
||||||
</UserPreferencesWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UserPreferences
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { useMutation, useQuery } from '@apollo/client'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Dropdown } from 'semantic-ui-react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { LanguageTranslation } from '../../../__generated__/globalTypes'
|
||||||
|
import {
|
||||||
|
InputLabelDescription,
|
||||||
|
InputLabelTitle,
|
||||||
|
SectionTitle,
|
||||||
|
} from './SettingsPage'
|
||||||
|
import {
|
||||||
|
changeUserPreferences,
|
||||||
|
changeUserPreferencesVariables,
|
||||||
|
} from './__generated__/changeUserPreferences'
|
||||||
|
import { myUserPreferences } from './__generated__/myUserPreferences'
|
||||||
|
|
||||||
|
const languagePreferences = [
|
||||||
|
{ key: 1, text: 'English', flag: 'uk', value: LanguageTranslation.English },
|
||||||
|
{ key: 2, text: 'Dansk', flag: 'dk', value: LanguageTranslation.Danish },
|
||||||
|
]
|
||||||
|
|
||||||
|
const CHANGE_USER_PREFERENCES = gql`
|
||||||
|
mutation changeUserPreferences($language: String) {
|
||||||
|
changeUserPreferences(language: $language) {
|
||||||
|
id
|
||||||
|
language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const MY_USER_PREFERENCES = gql`
|
||||||
|
query myUserPreferences {
|
||||||
|
myUserPreferences {
|
||||||
|
id
|
||||||
|
language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const UserPreferencesWrapper = styled.div`
|
||||||
|
margin-bottom: 24px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const UserPreferences = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { data } = useQuery<myUserPreferences>(MY_USER_PREFERENCES)
|
||||||
|
|
||||||
|
const [changePrefs, { loading: loadingPrefs, error }] = useMutation<
|
||||||
|
changeUserPreferences,
|
||||||
|
changeUserPreferencesVariables
|
||||||
|
>(CHANGE_USER_PREFERENCES)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>{error.message}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserPreferencesWrapper>
|
||||||
|
<SectionTitle nospace>
|
||||||
|
{t('settings.user_preferences.title', 'User preferences')}
|
||||||
|
</SectionTitle>
|
||||||
|
<label id="user_pref_change_language_field">
|
||||||
|
<InputLabelTitle>
|
||||||
|
{t(
|
||||||
|
'settings.user_preferences.change_language.label',
|
||||||
|
'Website language'
|
||||||
|
)}
|
||||||
|
</InputLabelTitle>
|
||||||
|
<InputLabelDescription>
|
||||||
|
{t(
|
||||||
|
'settings.user_preferences.change_language.description',
|
||||||
|
'Change website language specific for this user'
|
||||||
|
)}
|
||||||
|
</InputLabelDescription>
|
||||||
|
</label>
|
||||||
|
<Dropdown
|
||||||
|
id="user_pref_change_language_field"
|
||||||
|
placeholder={t(
|
||||||
|
'settings.user_preferences.language_selector.placeholder',
|
||||||
|
'Select language'
|
||||||
|
)}
|
||||||
|
clearable
|
||||||
|
options={languagePreferences}
|
||||||
|
onChange={(event, { value: language }) => {
|
||||||
|
changePrefs({
|
||||||
|
variables: {
|
||||||
|
language: language as LanguageTranslation,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
selection
|
||||||
|
search
|
||||||
|
value={data?.myUserPreferences.language || undefined}
|
||||||
|
loading={loadingPrefs}
|
||||||
|
disabled={loadingPrefs}
|
||||||
|
/>
|
||||||
|
</UserPreferencesWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserPreferences
|
|
@ -1,10 +1,9 @@
|
||||||
import { gql, useMutation } from '@apollo/client'
|
import { gql, useMutation } from '@apollo/client'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button, Checkbox, Input, Table } from 'semantic-ui-react'
|
import { Button, Checkbox, Input, Table } from 'semantic-ui-react'
|
||||||
|
|
||||||
const createUserMutation = gql`
|
const CREATE_USER_MUTATION = gql`
|
||||||
mutation createUser($username: String!, $admin: Boolean!) {
|
mutation createUser($username: String!, $admin: Boolean!) {
|
||||||
createUser(username: $username, admin: $admin) {
|
createUser(username: $username, admin: $admin) {
|
||||||
id
|
id
|
||||||
|
@ -15,7 +14,7 @@ const createUserMutation = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const addRootPathMutation = gql`
|
export const USER_ADD_ROOT_PATH_MUTATION = gql`
|
||||||
mutation userAddRootPath($id: ID!, $rootPath: String!) {
|
mutation userAddRootPath($id: ID!, $rootPath: String!) {
|
||||||
userAddRootPath(id: $id, rootPath: $rootPath) {
|
userAddRootPath(id: $id, rootPath: $rootPath) {
|
||||||
id
|
id
|
||||||
|
@ -30,12 +29,18 @@ const initialState = {
|
||||||
userAdded: false,
|
userAdded: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddUserRow = ({ setShow, show, onUserAdded }) => {
|
type AddUserRowProps = {
|
||||||
|
setShow: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
show: boolean
|
||||||
|
onUserAdded(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddUserRow = ({ setShow, show, onUserAdded }: AddUserRowProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [state, setState] = useState(initialState)
|
const [state, setState] = useState(initialState)
|
||||||
|
|
||||||
const [addRootPath, { loading: addRootPathLoading }] = useMutation(
|
const [addRootPath, { loading: addRootPathLoading }] = useMutation(
|
||||||
addRootPathMutation,
|
USER_ADD_ROOT_PATH_MUTATION,
|
||||||
{
|
{
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
setState(initialState)
|
setState(initialState)
|
||||||
|
@ -49,7 +54,7 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const [createUser, { loading: createUserLoading }] = useMutation(
|
const [createUser, { loading: createUserLoading }] = useMutation(
|
||||||
createUserMutation,
|
CREATE_USER_MUTATION,
|
||||||
{
|
{
|
||||||
onCompleted: ({ createUser: { id } }) => {
|
onCompleted: ({ createUser: { id } }) => {
|
||||||
if (state.rootPath) {
|
if (state.rootPath) {
|
||||||
|
@ -68,7 +73,10 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => {
|
||||||
|
|
||||||
const loading = addRootPathLoading || createUserLoading
|
const loading = addRootPathLoading || createUserLoading
|
||||||
|
|
||||||
function updateInput(event, key) {
|
function updateInput(
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
key: string
|
||||||
|
) {
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
[key]: event.target.value,
|
[key]: event.target.value,
|
||||||
|
@ -105,7 +113,7 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => {
|
||||||
onChange={(e, data) => {
|
onChange={(e, data) => {
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
admin: data.checked,
|
admin: data.checked || false,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -137,10 +145,4 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AddUserRow.propTypes = {
|
|
||||||
setShow: PropTypes.func.isRequired,
|
|
||||||
show: PropTypes.bool.isRequired,
|
|
||||||
onUserAdded: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AddUserRow
|
export default AddUserRow
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button, Checkbox, Input, Table } from 'semantic-ui-react'
|
import { Button, Checkbox, Input, Table } from 'semantic-ui-react'
|
||||||
import { EditRootPaths } from './EditUserRowRootPaths'
|
import { EditRootPaths } from './EditUserRowRootPaths'
|
||||||
import { UserRowProps } from './UserRow'
|
import { UserRowProps, UserRowChildProps } from './UserRow'
|
||||||
|
|
||||||
const EditUserRow = ({
|
const EditUserRow = ({
|
||||||
user,
|
user,
|
||||||
|
@ -10,9 +10,13 @@ const EditUserRow = ({
|
||||||
setState,
|
setState,
|
||||||
updateUser,
|
updateUser,
|
||||||
updateUserLoading,
|
updateUserLoading,
|
||||||
}) => {
|
}: UserRowChildProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
function updateInput(event, key) {
|
|
||||||
|
function updateInput(
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
key: string
|
||||||
|
) {
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
[key]: event.target.value,
|
[key]: event.target.value,
|
||||||
|
@ -39,7 +43,7 @@ const EditUserRow = ({
|
||||||
onChange={(_, data) => {
|
onChange={(_, data) => {
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
admin: data.checked,
|
admin: data.checked || false,
|
||||||
}))
|
}))
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -50,6 +54,7 @@ const EditUserRow = ({
|
||||||
negative
|
negative
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
|
...state,
|
||||||
...state.oldState,
|
...state.oldState,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
|
@ -1,20 +1,21 @@
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { gql, useMutation } from '@apollo/client'
|
import { gql, useMutation } from '@apollo/client'
|
||||||
import { Button, Icon, Input } from 'semantic-ui-react'
|
import { Button, Icon, Input } from 'semantic-ui-react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { USERS_QUERY } from './UsersTable'
|
import { USERS_QUERY } from './UsersTable'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { USER_ADD_ROOT_PATH_MUTATION } from './AddUserRow'
|
||||||
|
import {
|
||||||
|
userRemoveAlbumPathMutation,
|
||||||
|
userRemoveAlbumPathMutationVariables,
|
||||||
|
} from './__generated__/userRemoveAlbumPathMutation'
|
||||||
|
import {
|
||||||
|
settingsUsersQuery_user,
|
||||||
|
settingsUsersQuery_user_rootAlbums,
|
||||||
|
} from './__generated__/settingsUsersQuery'
|
||||||
|
import { userAddRootPath } from './__generated__/userAddRootPath'
|
||||||
|
|
||||||
const userAddRootPathMutation = gql`
|
const USER_REMOVE_ALBUM_PATH_MUTATION = gql`
|
||||||
mutation userAddRootPath($id: ID!, $rootPath: String!) {
|
|
||||||
userAddRootPath(id: $id, rootPath: $rootPath) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const userRemoveAlbumPathMutation = gql`
|
|
||||||
mutation userRemoveAlbumPathMutation($userId: ID!, $albumId: ID!) {
|
mutation userRemoveAlbumPathMutation($userId: ID!, $albumId: ID!) {
|
||||||
userRemoveRootAlbum(userId: $userId, albumId: $albumId) {
|
userRemoveRootAlbum(userId: $userId, albumId: $albumId) {
|
||||||
id
|
id
|
||||||
|
@ -28,18 +29,23 @@ const RootPathListItem = styled.li`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`
|
`
|
||||||
|
|
||||||
const EditRootPath = ({ album, user }) => {
|
type EditRootPathProps = {
|
||||||
|
album: settingsUsersQuery_user_rootAlbums
|
||||||
|
user: settingsUsersQuery_user
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditRootPath = ({ album, user }: EditRootPathProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [removeAlbumPath, { loading }] = useMutation(
|
const [removeAlbumPath, { loading }] = useMutation<
|
||||||
userRemoveAlbumPathMutation,
|
userRemoveAlbumPathMutation,
|
||||||
{
|
userRemoveAlbumPathMutationVariables
|
||||||
refetchQueries: [
|
>(USER_REMOVE_ALBUM_PATH_MUTATION, {
|
||||||
{
|
refetchQueries: [
|
||||||
query: USERS_QUERY,
|
{
|
||||||
},
|
query: USERS_QUERY,
|
||||||
],
|
},
|
||||||
}
|
],
|
||||||
)
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RootPathListItem>
|
<RootPathListItem>
|
||||||
|
@ -63,33 +69,37 @@ const EditRootPath = ({ album, user }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
EditRootPath.propTypes = {
|
|
||||||
album: PropTypes.object.isRequired,
|
|
||||||
user: PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
const NewRootPathInput = styled(Input)`
|
const NewRootPathInput = styled(Input)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const EditNewRootPath = ({ userID }) => {
|
type EditNewRootPathProps = {
|
||||||
|
userID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditNewRootPath = ({ userID }: EditNewRootPathProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [value, setValue] = useState('')
|
const [value, setValue] = useState('')
|
||||||
const [addRootPath, { loading }] = useMutation(userAddRootPathMutation, {
|
const [addRootPath, { loading }] = useMutation<userAddRootPath>(
|
||||||
refetchQueries: [
|
USER_ADD_ROOT_PATH_MUTATION,
|
||||||
{
|
{
|
||||||
query: USERS_QUERY,
|
refetchQueries: [
|
||||||
},
|
{
|
||||||
],
|
query: USERS_QUERY,
|
||||||
})
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<NewRootPathInput
|
<NewRootPathInput
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => setValue(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setValue(e.target.value)
|
||||||
|
}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
action={{
|
action={{
|
||||||
positive: true,
|
positive: true,
|
||||||
|
@ -110,17 +120,17 @@ const EditNewRootPath = ({ userID }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
EditNewRootPath.propTypes = {
|
|
||||||
userID: PropTypes.string.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
const RootPathList = styled.ul`
|
const RootPathList = styled.ul`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const EditRootPaths = ({ user }) => {
|
type EditRootPathsProps = {
|
||||||
|
user: settingsUsersQuery_user
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditRootPaths = ({ user }: EditRootPathsProps) => {
|
||||||
const editRows = user.rootAlbums.map(album => (
|
const editRows = user.rootAlbums.map(album => (
|
||||||
<EditRootPath key={album.id} album={album} user={user} />
|
<EditRootPath key={album.id} album={album} user={user} />
|
||||||
))
|
))
|
||||||
|
@ -132,7 +142,3 @@ export const EditRootPaths = ({ user }) => {
|
||||||
</RootPathList>
|
</RootPathList>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
EditRootPaths.propTypes = {
|
|
||||||
user: PropTypes.object.isRequired,
|
|
||||||
}
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { gql, useMutation } from '@apollo/client'
|
import { gql, useMutation } from '@apollo/client'
|
||||||
import { Button, Form, Input, Modal } from 'semantic-ui-react'
|
import { Button, Form, Input, Modal, ModalProps } from 'semantic-ui-react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { settingsUsersQuery_user } from './__generated__/settingsUsersQuery'
|
||||||
|
|
||||||
const changeUserPasswordMutation = gql`
|
const changeUserPasswordMutation = gql`
|
||||||
mutation changeUserPassword($userId: ID!, $password: String!) {
|
mutation changeUserPassword($userId: ID!, $password: String!) {
|
||||||
|
@ -12,7 +12,18 @@ const changeUserPasswordMutation = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const ChangePasswordModal = ({ onClose, user, ...props }) => {
|
interface ChangePasswordModalProps extends ModalProps {
|
||||||
|
onClose(): void
|
||||||
|
open: boolean
|
||||||
|
user: settingsUsersQuery_user
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChangePasswordModal = ({
|
||||||
|
onClose,
|
||||||
|
user,
|
||||||
|
open,
|
||||||
|
...props
|
||||||
|
}: ChangePasswordModalProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [passwordInput, setPasswordInput] = useState('')
|
const [passwordInput, setPasswordInput] = useState('')
|
||||||
|
|
||||||
|
@ -23,7 +34,7 @@ const ChangePasswordModal = ({ onClose, user, ...props }) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal {...props}>
|
<Modal open={open} {...props}>
|
||||||
<Modal.Header>
|
<Modal.Header>
|
||||||
{t('settings.users.password_reset.title', 'Change password')}
|
{t('settings.users.password_reset.title', 'Change password')}
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
|
@ -71,9 +82,4 @@ const ChangePasswordModal = ({ onClose, user, ...props }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangePasswordModal.propTypes = {
|
|
||||||
onClose: PropTypes.func,
|
|
||||||
user: PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ChangePasswordModal
|
export default ChangePasswordModal
|
|
@ -1,111 +0,0 @@
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { gql, useMutation } from '@apollo/client'
|
|
||||||
import EditUserRow from './EditUserRow'
|
|
||||||
import ViewUserRow from './ViewUserRow'
|
|
||||||
|
|
||||||
const updateUserMutation = gql`
|
|
||||||
mutation updateUser($id: ID!, $username: String, $admin: Boolean) {
|
|
||||||
updateUser(id: $id, username: $username, admin: $admin) {
|
|
||||||
id
|
|
||||||
username
|
|
||||||
admin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const deleteUserMutation = gql`
|
|
||||||
mutation deleteUser($id: ID!) {
|
|
||||||
deleteUser(id: $id) {
|
|
||||||
id
|
|
||||||
username
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const scanUserMutation = gql`
|
|
||||||
mutation scanUser($userId: ID!) {
|
|
||||||
scanUser(userId: $userId) {
|
|
||||||
success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const UserRow = ({ user, refetchUsers }) => {
|
|
||||||
const [state, setState] = useState({
|
|
||||||
...user,
|
|
||||||
editing: false,
|
|
||||||
newRootPath: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const [showConfirmDelete, setConfirmDelete] = useState(false)
|
|
||||||
const [showChangePassword, setChangePassword] = useState(false)
|
|
||||||
|
|
||||||
const [updateUser, { loading: updateUserLoading }] = useMutation(
|
|
||||||
updateUserMutation,
|
|
||||||
{
|
|
||||||
onCompleted: data => {
|
|
||||||
setState({
|
|
||||||
...data.updateUser,
|
|
||||||
editing: false,
|
|
||||||
})
|
|
||||||
refetchUsers()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const [deleteUser] = useMutation(deleteUserMutation, {
|
|
||||||
onCompleted: () => {
|
|
||||||
refetchUsers()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const [scanUser, { called: scanUserCalled }] = useMutation(scanUserMutation, {
|
|
||||||
onCompleted: () => {
|
|
||||||
refetchUsers()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
user,
|
|
||||||
state,
|
|
||||||
setState,
|
|
||||||
scanUser,
|
|
||||||
updateUser,
|
|
||||||
updateUserLoading,
|
|
||||||
deleteUser,
|
|
||||||
setChangePassword,
|
|
||||||
setConfirmDelete,
|
|
||||||
scanUserCalled,
|
|
||||||
showChangePassword,
|
|
||||||
showConfirmDelete,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.editing) {
|
|
||||||
return <EditUserRow {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return <ViewUserRow {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
UserRow.propTypes = {
|
|
||||||
user: PropTypes.object.isRequired,
|
|
||||||
refetchUsers: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserRowProps = {
|
|
||||||
user: PropTypes.object.isRequired,
|
|
||||||
state: PropTypes.object.isRequired,
|
|
||||||
setState: PropTypes.func.isRequired,
|
|
||||||
scanUser: PropTypes.func.isRequired,
|
|
||||||
updateUser: PropTypes.func.isRequired,
|
|
||||||
updateUserLoading: PropTypes.bool.isRequired,
|
|
||||||
deleteUser: PropTypes.func.isRequired,
|
|
||||||
setChangePassword: PropTypes.func.isRequired,
|
|
||||||
setConfirmDelete: PropTypes.func.isRequired,
|
|
||||||
scanUserCalled: PropTypes.func.isRequired,
|
|
||||||
showChangePassword: PropTypes.func.isRequired,
|
|
||||||
showConfirmDelete: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UserRow
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import {
|
||||||
|
FetchResult,
|
||||||
|
gql,
|
||||||
|
MutationFunctionOptions,
|
||||||
|
useMutation,
|
||||||
|
} from '@apollo/client'
|
||||||
|
import EditUserRow from './EditUserRow'
|
||||||
|
import ViewUserRow from './ViewUserRow'
|
||||||
|
import { settingsUsersQuery_user } from './__generated__/settingsUsersQuery'
|
||||||
|
import { scanUser, scanUserVariables } from './__generated__/scanUser'
|
||||||
|
import { updateUser, updateUserVariables } from './__generated__/updateUser'
|
||||||
|
import { deleteUser, deleteUserVariables } from './__generated__/deleteUser'
|
||||||
|
|
||||||
|
const updateUserMutation = gql`
|
||||||
|
mutation updateUser($id: ID!, $username: String, $admin: Boolean) {
|
||||||
|
updateUser(id: $id, username: $username, admin: $admin) {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
admin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const deleteUserMutation = gql`
|
||||||
|
mutation deleteUser($id: ID!) {
|
||||||
|
deleteUser(id: $id) {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const scanUserMutation = gql`
|
||||||
|
mutation scanUser($userId: ID!) {
|
||||||
|
scanUser(userId: $userId) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const UserRowProps = {
|
||||||
|
user: PropTypes.object.isRequired,
|
||||||
|
state: PropTypes.object.isRequired,
|
||||||
|
setState: PropTypes.func.isRequired,
|
||||||
|
scanUser: PropTypes.func.isRequired,
|
||||||
|
updateUser: PropTypes.func.isRequired,
|
||||||
|
updateUserLoading: PropTypes.bool.isRequired,
|
||||||
|
deleteUser: PropTypes.func.isRequired,
|
||||||
|
setChangePassword: PropTypes.func.isRequired,
|
||||||
|
setConfirmDelete: PropTypes.func.isRequired,
|
||||||
|
scanUserCalled: PropTypes.func.isRequired,
|
||||||
|
showChangePassword: PropTypes.func.isRequired,
|
||||||
|
showConfirmDelete: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserRowState extends settingsUsersQuery_user {
|
||||||
|
editing: boolean
|
||||||
|
newRootPath: string
|
||||||
|
oldState?: Omit<UserRowState, 'oldState'>
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApolloMutationFn<MutationType, VariablesType> = (
|
||||||
|
options?: MutationFunctionOptions<MutationType, VariablesType>
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
) => Promise<FetchResult<MutationType, any, any>>
|
||||||
|
|
||||||
|
export type UserRowChildProps = {
|
||||||
|
user: settingsUsersQuery_user
|
||||||
|
state: UserRowState
|
||||||
|
setState: React.Dispatch<React.SetStateAction<UserRowState>>
|
||||||
|
scanUser: ApolloMutationFn<scanUser, scanUserVariables>
|
||||||
|
updateUser: ApolloMutationFn<updateUser, updateUserVariables>
|
||||||
|
updateUserLoading: boolean
|
||||||
|
deleteUser: ApolloMutationFn<deleteUser, deleteUserVariables>
|
||||||
|
setChangePassword: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
setConfirmDelete: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
scanUserCalled: boolean
|
||||||
|
showChangePassword: boolean
|
||||||
|
showConfirmDelete: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserRowProps = {
|
||||||
|
user: settingsUsersQuery_user
|
||||||
|
refetchUsers(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserRow = ({ user, refetchUsers }: UserRowProps) => {
|
||||||
|
const [state, setState] = useState<UserRowState>({
|
||||||
|
...user,
|
||||||
|
editing: false,
|
||||||
|
newRootPath: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const [showConfirmDelete, setConfirmDelete] = useState(false)
|
||||||
|
const [showChangePassword, setChangePassword] = useState(false)
|
||||||
|
|
||||||
|
const [updateUser, { loading: updateUserLoading }] = useMutation<
|
||||||
|
updateUser,
|
||||||
|
updateUserVariables
|
||||||
|
>(updateUserMutation, {
|
||||||
|
onCompleted: data => {
|
||||||
|
setState(state => ({
|
||||||
|
...state,
|
||||||
|
...data.updateUser,
|
||||||
|
editing: false,
|
||||||
|
}))
|
||||||
|
refetchUsers()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const [deleteUser] = useMutation<deleteUser, deleteUserVariables>(
|
||||||
|
deleteUserMutation,
|
||||||
|
{
|
||||||
|
onCompleted: () => {
|
||||||
|
refetchUsers()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const [scanUser, { called: scanUserCalled }] = useMutation<
|
||||||
|
scanUser,
|
||||||
|
scanUserVariables
|
||||||
|
>(scanUserMutation, {
|
||||||
|
onCompleted: () => {
|
||||||
|
refetchUsers()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const props: UserRowChildProps = {
|
||||||
|
user,
|
||||||
|
state,
|
||||||
|
setState,
|
||||||
|
scanUser,
|
||||||
|
updateUser,
|
||||||
|
updateUserLoading,
|
||||||
|
deleteUser,
|
||||||
|
setChangePassword,
|
||||||
|
setConfirmDelete,
|
||||||
|
scanUserCalled,
|
||||||
|
showChangePassword,
|
||||||
|
showConfirmDelete,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.editing) {
|
||||||
|
return <EditUserRow {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ViewUserRow {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserRow
|
|
@ -6,13 +6,13 @@ import UserRow from './UserRow'
|
||||||
import AddUserRow from './AddUserRow'
|
import AddUserRow from './AddUserRow'
|
||||||
import { SectionTitle } from '../SettingsPage'
|
import { SectionTitle } from '../SettingsPage'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { settingsUsersQuery } from './__generated__/settingsUsersQuery'
|
||||||
|
|
||||||
export const USERS_QUERY = gql`
|
export const USERS_QUERY = gql`
|
||||||
query settingsUsersQuery {
|
query settingsUsersQuery {
|
||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
username
|
username
|
||||||
# rootPath
|
|
||||||
admin
|
admin
|
||||||
rootAlbums {
|
rootAlbums {
|
||||||
id
|
id
|
||||||
|
@ -26,14 +26,16 @@ const UsersTable = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [showAddUser, setShowAddUser] = useState(false)
|
const [showAddUser, setShowAddUser] = useState(false)
|
||||||
|
|
||||||
const { loading, error, data, refetch } = useQuery(USERS_QUERY)
|
const { loading, error, data, refetch } = useQuery<settingsUsersQuery>(
|
||||||
|
USERS_QUERY
|
||||||
|
)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return `Users table error: ${error.message}`
|
return <div>{`Users table error: ${error.message}`}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
let userRows = []
|
let userRows: JSX.Element[] = []
|
||||||
if (data && data.user) {
|
if (data?.user) {
|
||||||
userRows = data.user.map(user => (
|
userRows = data.user.map(user => (
|
||||||
<UserRow user={user} refetchUsers={refetch} key={user.id} />
|
<UserRow user={user} refetchUsers={refetch} key={user.id} />
|
||||||
))
|
))
|
|
@ -3,7 +3,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { Button, Icon, Table, Modal } from 'semantic-ui-react'
|
import { Button, Icon, Table, Modal } from 'semantic-ui-react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import ChangePasswordModal from './UserChangePassword'
|
import ChangePasswordModal from './UserChangePassword'
|
||||||
import { UserRowProps } from './UserRow'
|
import { UserRowChildProps, UserRowProps } from './UserRow'
|
||||||
|
|
||||||
const PathList = styled.ul`
|
const PathList = styled.ul`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -22,7 +22,7 @@ const ViewUserRow = ({
|
||||||
scanUserCalled,
|
scanUserCalled,
|
||||||
showChangePassword,
|
showChangePassword,
|
||||||
showConfirmDelete,
|
showConfirmDelete,
|
||||||
}) => {
|
}: UserRowChildProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const paths = (
|
const paths = (
|
||||||
<PathList>
|
<PathList>
|
||||||
|
@ -43,7 +43,11 @@ const ViewUserRow = ({
|
||||||
<Button.Group>
|
<Button.Group>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setState(state => ({ ...state, editing: true, oldState: state }))
|
setState(state => {
|
||||||
|
const oldState = { ...state }
|
||||||
|
delete oldState.oldState
|
||||||
|
return { ...state, editing: true, oldState }
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="edit" />
|
<Icon name="edit" />
|
|
@ -0,0 +1,22 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: changeUserPassword
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface changeUserPassword_updateUser {
|
||||||
|
__typename: 'User'
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface changeUserPassword {
|
||||||
|
updateUser: changeUserPassword_updateUser
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface changeUserPasswordVariables {
|
||||||
|
userId: string
|
||||||
|
password: string
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: createUser
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface createUser_createUser {
|
||||||
|
__typename: 'User'
|
||||||
|
id: string
|
||||||
|
username: string
|
||||||
|
admin: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface createUser {
|
||||||
|
createUser: createUser_createUser
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface createUserVariables {
|
||||||
|
username: string
|
||||||
|
admin: boolean
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: deleteUser
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface deleteUser_deleteUser {
|
||||||
|
__typename: 'User'
|
||||||
|
id: string
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface deleteUser {
|
||||||
|
deleteUser: deleteUser_deleteUser
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface deleteUserVariables {
|
||||||
|
id: string
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: scanUser
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface scanUser_scanUser {
|
||||||
|
__typename: "ScannerResult";
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface scanUser {
|
||||||
|
/**
|
||||||
|
* Scan a single user for new media
|
||||||
|
*/
|
||||||
|
scanUser: scanUser_scanUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface scanUserVariables {
|
||||||
|
userId: string;
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: settingsUsersQuery
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface settingsUsersQuery_user_rootAlbums {
|
||||||
|
__typename: "Album";
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The path on the filesystem of the server, where this album is located
|
||||||
|
*/
|
||||||
|
filePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface settingsUsersQuery_user {
|
||||||
|
__typename: "User";
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
admin: boolean;
|
||||||
|
/**
|
||||||
|
* Top level albums owned by this user
|
||||||
|
*/
|
||||||
|
rootAlbums: settingsUsersQuery_user_rootAlbums[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface settingsUsersQuery {
|
||||||
|
/**
|
||||||
|
* List of registered users, must be admin to call
|
||||||
|
*/
|
||||||
|
user: settingsUsersQuery_user[];
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: updateUser
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface updateUser_updateUser {
|
||||||
|
__typename: 'User'
|
||||||
|
id: string
|
||||||
|
username: string
|
||||||
|
admin: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface updateUser {
|
||||||
|
updateUser: updateUser_updateUser
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface updateUserVariables {
|
||||||
|
id: string
|
||||||
|
username?: string | null
|
||||||
|
admin?: boolean | null
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: userAddRootPath
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface userAddRootPath_userAddRootPath {
|
||||||
|
__typename: "Album";
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface userAddRootPath {
|
||||||
|
/**
|
||||||
|
* Add a root path from where to look for media for the given user
|
||||||
|
*/
|
||||||
|
userAddRootPath: userAddRootPath_userAddRootPath | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface userAddRootPathVariables {
|
||||||
|
id: string;
|
||||||
|
rootPath: string;
|
||||||
|
}
|
22
ui/src/Pages/SettingsPage/Users/__generated__/userRemoveAlbumPathMutation.ts
generated
Normal file
22
ui/src/Pages/SettingsPage/Users/__generated__/userRemoveAlbumPathMutation.ts
generated
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: userRemoveAlbumPathMutation
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface userRemoveAlbumPathMutation_userRemoveRootAlbum {
|
||||||
|
__typename: "Album";
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface userRemoveAlbumPathMutation {
|
||||||
|
userRemoveRootAlbum: userRemoveAlbumPathMutation_userRemoveRootAlbum | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface userRemoveAlbumPathMutationVariables {
|
||||||
|
userId: string;
|
||||||
|
albumId: string;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: changeScanIntervalMutation
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface changeScanIntervalMutation {
|
||||||
|
/**
|
||||||
|
* Set how often, in seconds, the server should automatically scan for new media,
|
||||||
|
* a value of 0 will disable periodic scans
|
||||||
|
*/
|
||||||
|
setPeriodicScanInterval: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface changeScanIntervalMutationVariables {
|
||||||
|
interval: number;
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { LanguageTranslation } from "./../../../../__generated__/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: changeUserPreferences
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface changeUserPreferences_changeUserPreferences {
|
||||||
|
__typename: "UserPreferences";
|
||||||
|
id: string;
|
||||||
|
language: LanguageTranslation | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface changeUserPreferences {
|
||||||
|
changeUserPreferences: changeUserPreferences_changeUserPreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface changeUserPreferencesVariables {
|
||||||
|
language?: string | null;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: concurrentWorkersQuery
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface concurrentWorkersQuery_siteInfo {
|
||||||
|
__typename: "SiteInfo";
|
||||||
|
/**
|
||||||
|
* How many max concurrent scanner jobs that should run at once
|
||||||
|
*/
|
||||||
|
concurrentWorkers: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface concurrentWorkersQuery {
|
||||||
|
siteInfo: concurrentWorkersQuery_siteInfo;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { LanguageTranslation } from "./../../../../__generated__/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: myUserPreferences
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface myUserPreferences_myUserPreferences {
|
||||||
|
__typename: "UserPreferences";
|
||||||
|
id: string;
|
||||||
|
language: LanguageTranslation | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface myUserPreferences {
|
||||||
|
myUserPreferences: myUserPreferences_myUserPreferences;
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: scanAllMutation
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface scanAllMutation_scanAll {
|
||||||
|
__typename: "ScannerResult";
|
||||||
|
success: boolean;
|
||||||
|
message: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface scanAllMutation {
|
||||||
|
/**
|
||||||
|
* Scan all users for new media
|
||||||
|
*/
|
||||||
|
scanAll: scanAllMutation_scanAll;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: scanIntervalQuery
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface scanIntervalQuery_siteInfo {
|
||||||
|
__typename: "SiteInfo";
|
||||||
|
/**
|
||||||
|
* How often automatic scans should be initiated in seconds
|
||||||
|
*/
|
||||||
|
periodicScanInterval: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface scanIntervalQuery {
|
||||||
|
siteInfo: scanIntervalQuery_siteInfo;
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: setConcurrentWorkers
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface setConcurrentWorkers {
|
||||||
|
/**
|
||||||
|
* Set max number of concurrent scanner jobs running at once
|
||||||
|
*/
|
||||||
|
setScannerConcurrentWorkers: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface setConcurrentWorkersVariables {
|
||||||
|
workers: number;
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Layout from '../../Layout'
|
import Layout from '../../Layout'
|
||||||
import AlbumGallery from '../../components/albumGallery/AlbumGallery'
|
import AlbumGallery from '../../components/albumGallery/AlbumGallery'
|
||||||
|
@ -73,7 +72,13 @@ const AlbumSharePageWrapper = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`
|
`
|
||||||
|
|
||||||
const AlbumSharePage = ({ albumID, token, password }) => {
|
type AlbumSharePageProps = {
|
||||||
|
albumID: string
|
||||||
|
token: string
|
||||||
|
password: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlbumSharePage = ({ albumID, token, password }: AlbumSharePageProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { data, loading, error } = useQuery(SHARE_ALBUM_QUERY, {
|
const { data, loading, error } = useQuery(SHARE_ALBUM_QUERY, {
|
||||||
variables: {
|
variables: {
|
||||||
|
@ -86,11 +91,11 @@ const AlbumSharePage = ({ albumID, token, password }) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return error.message
|
return <div>{error.message}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return t('general.loading.default', 'Loading...')
|
return <div>{t('general.loading.default', 'Loading...')}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
const album = data.album
|
const album = data.album
|
||||||
|
@ -111,10 +116,4 @@ const AlbumSharePage = ({ albumID, token, password }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AlbumSharePage.propTypes = {
|
|
||||||
albumID: PropTypes.string.isRequired,
|
|
||||||
token: PropTypes.string.isRequired,
|
|
||||||
password: PropTypes.string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AlbumSharePage
|
export default AlbumSharePage
|
|
@ -1,58 +0,0 @@
|
||||||
import React, { useContext, useEffect } from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
import Layout from '../../Layout'
|
|
||||||
import {
|
|
||||||
ProtectedImage,
|
|
||||||
ProtectedVideo,
|
|
||||||
} from '../../components/photoGallery/ProtectedMedia'
|
|
||||||
import { SidebarContext } from '../../components/sidebar/Sidebar'
|
|
||||||
import MediaSidebar from '../../components/sidebar/MediaSidebar'
|
|
||||||
|
|
||||||
const DisplayPhoto = styled(ProtectedImage)`
|
|
||||||
width: 100%;
|
|
||||||
max-height: calc(80vh);
|
|
||||||
object-fit: contain;
|
|
||||||
`
|
|
||||||
|
|
||||||
const DisplayVideo = styled(ProtectedVideo)`
|
|
||||||
width: 100%;
|
|
||||||
max-height: calc(80vh);
|
|
||||||
`
|
|
||||||
|
|
||||||
const MediaView = ({ media }) => {
|
|
||||||
const { updateSidebar } = useContext(SidebarContext)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateSidebar(<MediaSidebar media={media} hidePreview />)
|
|
||||||
}, [media])
|
|
||||||
|
|
||||||
if (media.type == 'photo') {
|
|
||||||
return <DisplayPhoto src={media.highRes.url} />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media.type == 'video') {
|
|
||||||
return <DisplayVideo media={media} />
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unsupported media type: ${media.type}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaView.propTypes = {
|
|
||||||
media: PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
const MediaSharePage = ({ media }) => (
|
|
||||||
<Layout>
|
|
||||||
<div data-testid="MediaSharePage">
|
|
||||||
<h1>{media.title}</h1>
|
|
||||||
<MediaView media={media} />
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
)
|
|
||||||
|
|
||||||
MediaSharePage.propTypes = {
|
|
||||||
media: PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MediaSharePage
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import React, { useContext, useEffect } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import Layout from '../../Layout'
|
||||||
|
import {
|
||||||
|
ProtectedImage,
|
||||||
|
ProtectedVideo,
|
||||||
|
} from '../../components/photoGallery/ProtectedMedia'
|
||||||
|
import { SidebarContext } from '../../components/sidebar/Sidebar'
|
||||||
|
import MediaSidebar from '../../components/sidebar/MediaSidebar'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { SharePageToken_shareToken_media } from './__generated__/SharePageToken'
|
||||||
|
import { MediaType } from '../../../__generated__/globalTypes'
|
||||||
|
|
||||||
|
const DisplayPhoto = styled(ProtectedImage)`
|
||||||
|
width: 100%;
|
||||||
|
max-height: calc(80vh);
|
||||||
|
object-fit: contain;
|
||||||
|
`
|
||||||
|
|
||||||
|
const DisplayVideo = styled(ProtectedVideo)`
|
||||||
|
width: 100%;
|
||||||
|
max-height: calc(80vh);
|
||||||
|
`
|
||||||
|
|
||||||
|
type MediaViewProps = {
|
||||||
|
media: SharePageToken_shareToken_media
|
||||||
|
}
|
||||||
|
|
||||||
|
const MediaView = ({ media }: MediaViewProps) => {
|
||||||
|
const { updateSidebar } = useContext(SidebarContext)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateSidebar(<MediaSidebar media={media} hidePreview />)
|
||||||
|
}, [media])
|
||||||
|
|
||||||
|
switch (media.type) {
|
||||||
|
case MediaType.Photo:
|
||||||
|
return <DisplayPhoto src={media.highRes?.url} />
|
||||||
|
case MediaType.Video:
|
||||||
|
return <DisplayVideo media={media} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaSharePageType = {
|
||||||
|
media: SharePageToken_shareToken_media
|
||||||
|
}
|
||||||
|
|
||||||
|
const MediaSharePage = ({ media }: MediaSharePageType) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout title={t('share_page.media.title', 'Shared media')}>
|
||||||
|
<div data-testid="MediaSharePage">
|
||||||
|
<h1>{media.title}</h1>
|
||||||
|
<MediaView media={media} />
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MediaSharePage
|
|
@ -82,7 +82,7 @@ describe('load correct share page, based on graphql query', () => {
|
||||||
media: {
|
media: {
|
||||||
id: '1',
|
id: '1',
|
||||||
title: 'shared_image.jpg',
|
title: 'shared_image.jpg',
|
||||||
type: 'photo',
|
type: 'Photo',
|
||||||
highRes: {
|
highRes: {
|
||||||
url: 'https://example.com/shared_image.jpg',
|
url: 'https://example.com/shared_image.jpg',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useQuery, gql } from '@apollo/client'
|
import { useQuery, gql } from '@apollo/client'
|
||||||
import { Route, Switch } from 'react-router-dom'
|
import { match as MatchType, Route, Switch } from 'react-router-dom'
|
||||||
import RouterProps from 'react-router-prop-types'
|
|
||||||
import { Form, Header, Icon, Input, Message } from 'semantic-ui-react'
|
import { Form, Header, Icon, Input, Message } from 'semantic-ui-react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {
|
import {
|
||||||
|
@ -45,8 +44,11 @@ export const SHARE_TOKEN_QUERY = gql`
|
||||||
}
|
}
|
||||||
videoWeb {
|
videoWeb {
|
||||||
url
|
url
|
||||||
|
width
|
||||||
|
height
|
||||||
}
|
}
|
||||||
exif {
|
exif {
|
||||||
|
id
|
||||||
camera
|
camera
|
||||||
maker
|
maker
|
||||||
lens
|
lens
|
||||||
|
@ -71,7 +73,7 @@ export const VALIDATE_TOKEN_PASSWORD_QUERY = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const AuthorizedTokenRoute = ({ match }) => {
|
const AuthorizedTokenRoute = ({ match }: MatchProps<TokenRouteMatch>) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const token = match.params.token
|
const token = match.params.token
|
||||||
|
@ -84,11 +86,11 @@ const AuthorizedTokenRoute = ({ match }) => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (error) return error.message
|
if (error) return <div>{error.message}</div>
|
||||||
if (loading) return 'Loading...'
|
if (loading) return <div>{t('general.loading.default', 'Loading...')}</div>
|
||||||
|
|
||||||
if (data.shareToken.album) {
|
if (data.shareToken.album) {
|
||||||
const SharedSubAlbumPage = ({ match }) => {
|
const SharedSubAlbumPage = ({ match }: MatchProps<SubalbumRouteMatch>) => {
|
||||||
return (
|
return (
|
||||||
<AlbumSharePage
|
<AlbumSharePage
|
||||||
albumID={match.params.subAlbum}
|
albumID={match.params.subAlbum}
|
||||||
|
@ -136,10 +138,15 @@ const MessageContainer = styled.div`
|
||||||
margin: 100px auto 0;
|
margin: 100px auto 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type ProtectedTokenEnterPasswordProps = {
|
||||||
|
refetchWithPassword(password: string): void
|
||||||
|
loading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const ProtectedTokenEnterPassword = ({
|
const ProtectedTokenEnterPassword = ({
|
||||||
refetchWithPassword,
|
refetchWithPassword,
|
||||||
loading = false,
|
loading = false,
|
||||||
}) => {
|
}: ProtectedTokenEnterPasswordProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const [passwordValue, setPasswordValue] = useState('')
|
const [passwordValue, setPasswordValue] = useState('')
|
||||||
|
@ -178,7 +185,9 @@ const ProtectedTokenEnterPassword = ({
|
||||||
<Input
|
<Input
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onKeyUp={event => event.key == 'Enter' && onSubmit()}
|
onKeyUp={(event: KeyboardEvent) =>
|
||||||
|
event.key == 'Enter' && onSubmit()
|
||||||
|
}
|
||||||
onChange={e => setPasswordValue(e.target.value)}
|
onChange={e => setPasswordValue(e.target.value)}
|
||||||
placeholder={t('login_page.field.password', 'Password')}
|
placeholder={t('login_page.field.password', 'Password')}
|
||||||
type="password"
|
type="password"
|
||||||
|
@ -191,12 +200,19 @@ const ProtectedTokenEnterPassword = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ProtectedTokenEnterPassword.propTypes = {
|
interface TokenRouteMatch {
|
||||||
refetchWithPassword: PropTypes.func.isRequired,
|
token: string
|
||||||
loading: PropTypes.bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TokenRoute = ({ match }) => {
|
interface SubalbumRouteMatch extends TokenRouteMatch {
|
||||||
|
subAlbum: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MatchProps<Route> {
|
||||||
|
match: MatchType<Route>
|
||||||
|
}
|
||||||
|
|
||||||
|
const TokenRoute = ({ match }: MatchProps<TokenRouteMatch>) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const token = match.params.token
|
const token = match.params.token
|
||||||
|
@ -227,13 +243,12 @@ const TokenRoute = ({ match }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return error.message
|
return <div>{error.message}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data && data.shareTokenValidatePassword == false) {
|
if (data && data.shareTokenValidatePassword == false) {
|
||||||
return (
|
return (
|
||||||
<ProtectedTokenEnterPassword
|
<ProtectedTokenEnterPassword
|
||||||
match={match}
|
|
||||||
refetchWithPassword={password => {
|
refetchWithPassword={password => {
|
||||||
saveSharePassword(token, password)
|
saveSharePassword(token, password)
|
||||||
refetch({ token, password })
|
refetch({ token, password })
|
||||||
|
@ -243,22 +258,18 @@ const TokenRoute = ({ match }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) return t('general.loading.default', 'Loading...')
|
if (loading) return <div>{t('general.loading.default', 'Loading...')}</div>
|
||||||
|
|
||||||
return <AuthorizedTokenRoute match={match} />
|
return <AuthorizedTokenRoute match={match} />
|
||||||
}
|
}
|
||||||
|
|
||||||
TokenRoute.propTypes = {
|
const SharePage = ({ match }: { match: MatchType }) => {
|
||||||
match: PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
const SharePage = ({ match }) => {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={`${match.url}/:token`}>
|
<Route path={`${match.url}/:token`}>
|
||||||
{({ match }) => {
|
{({ match }: { match: MatchType<TokenRouteMatch> }) => {
|
||||||
return <TokenRoute match={match} />
|
return <TokenRoute match={match} />
|
||||||
}}
|
}}
|
||||||
</Route>
|
</Route>
|
||||||
|
@ -267,8 +278,4 @@ const SharePage = ({ match }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SharePage.propTypes = {
|
|
||||||
...RouterProps,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SharePage
|
export default SharePage
|
|
@ -0,0 +1,174 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { MediaType } from './../../../../__generated__/globalTypes'
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: SharePageToken
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface SharePageToken_shareToken_album {
|
||||||
|
__typename: 'Album'
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharePageToken_shareToken_media_thumbnail {
|
||||||
|
__typename: 'MediaURL'
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharePageToken_shareToken_media_downloads_mediaUrl {
|
||||||
|
__typename: 'MediaURL'
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number
|
||||||
|
/**
|
||||||
|
* The file size of the resource in bytes
|
||||||
|
*/
|
||||||
|
fileSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharePageToken_shareToken_media_downloads {
|
||||||
|
__typename: 'MediaDownload'
|
||||||
|
title: string
|
||||||
|
mediaUrl: SharePageToken_shareToken_media_downloads_mediaUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharePageToken_shareToken_media_highRes {
|
||||||
|
__typename: 'MediaURL'
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharePageToken_shareToken_media_videoWeb {
|
||||||
|
__typename: 'MediaURL'
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharePageToken_shareToken_media_exif {
|
||||||
|
__typename: 'MediaEXIF'
|
||||||
|
id: string
|
||||||
|
/**
|
||||||
|
* The model name of the camera
|
||||||
|
*/
|
||||||
|
camera: string | null
|
||||||
|
/**
|
||||||
|
* The maker of the camera
|
||||||
|
*/
|
||||||
|
maker: string | null
|
||||||
|
/**
|
||||||
|
* The name of the lens
|
||||||
|
*/
|
||||||
|
lens: string | null
|
||||||
|
dateShot: any | null
|
||||||
|
/**
|
||||||
|
* The exposure time of the image
|
||||||
|
*/
|
||||||
|
exposure: number | null
|
||||||
|
/**
|
||||||
|
* The aperature stops of the image
|
||||||
|
*/
|
||||||
|
aperture: number | null
|
||||||
|
/**
|
||||||
|
* The ISO setting of the image
|
||||||
|
*/
|
||||||
|
iso: number | null
|
||||||
|
/**
|
||||||
|
* The focal length of the lens, when the image was taken
|
||||||
|
*/
|
||||||
|
focalLength: number | null
|
||||||
|
/**
|
||||||
|
* A formatted description of the flash settings, when the image was taken
|
||||||
|
*/
|
||||||
|
flash: number | null
|
||||||
|
/**
|
||||||
|
* An index describing the mode for adjusting the exposure of the image
|
||||||
|
*/
|
||||||
|
exposureProgram: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharePageToken_shareToken_media {
|
||||||
|
__typename: 'Media'
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
type: MediaType
|
||||||
|
/**
|
||||||
|
* URL to display the media in a smaller resolution
|
||||||
|
*/
|
||||||
|
thumbnail: SharePageToken_shareToken_media_thumbnail | null
|
||||||
|
downloads: SharePageToken_shareToken_media_downloads[]
|
||||||
|
/**
|
||||||
|
* URL to display the photo in full resolution, will be null for videos
|
||||||
|
*/
|
||||||
|
highRes: SharePageToken_shareToken_media_highRes | null
|
||||||
|
/**
|
||||||
|
* URL to get the video in a web format that can be played in the browser, will be null for photos
|
||||||
|
*/
|
||||||
|
videoWeb: SharePageToken_shareToken_media_videoWeb | null
|
||||||
|
exif: SharePageToken_shareToken_media_exif | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharePageToken_shareToken {
|
||||||
|
__typename: 'ShareToken'
|
||||||
|
token: string
|
||||||
|
/**
|
||||||
|
* The album this token shares
|
||||||
|
*/
|
||||||
|
album: SharePageToken_shareToken_album | null
|
||||||
|
/**
|
||||||
|
* The media this token shares
|
||||||
|
*/
|
||||||
|
media: SharePageToken_shareToken_media | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharePageToken {
|
||||||
|
shareToken: SharePageToken_shareToken
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharePageTokenVariables {
|
||||||
|
token: string
|
||||||
|
password?: string | null
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: ShareTokenValidatePassword
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface ShareTokenValidatePassword {
|
||||||
|
shareTokenValidatePassword: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShareTokenValidatePasswordVariables {
|
||||||
|
token: string;
|
||||||
|
password?: string | null;
|
||||||
|
}
|
|
@ -0,0 +1,194 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { MediaType } from "./../../../../__generated__/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: shareAlbumQuery
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface shareAlbumQuery_album_subAlbums_thumbnail_thumbnail {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface shareAlbumQuery_album_subAlbums_thumbnail {
|
||||||
|
__typename: "Media";
|
||||||
|
/**
|
||||||
|
* URL to display the media in a smaller resolution
|
||||||
|
*/
|
||||||
|
thumbnail: shareAlbumQuery_album_subAlbums_thumbnail_thumbnail | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface shareAlbumQuery_album_subAlbums {
|
||||||
|
__typename: "Album";
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* An image in this album used for previewing this album
|
||||||
|
*/
|
||||||
|
thumbnail: shareAlbumQuery_album_subAlbums_thumbnail | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface shareAlbumQuery_album_media_thumbnail {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number;
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface shareAlbumQuery_album_media_downloads_mediaUrl {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number;
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number;
|
||||||
|
/**
|
||||||
|
* The file size of the resource in bytes
|
||||||
|
*/
|
||||||
|
fileSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface shareAlbumQuery_album_media_downloads {
|
||||||
|
__typename: "MediaDownload";
|
||||||
|
title: string;
|
||||||
|
mediaUrl: shareAlbumQuery_album_media_downloads_mediaUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface shareAlbumQuery_album_media_highRes {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number;
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface shareAlbumQuery_album_media_videoWeb {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface shareAlbumQuery_album_media_exif {
|
||||||
|
__typename: "MediaEXIF";
|
||||||
|
/**
|
||||||
|
* The model name of the camera
|
||||||
|
*/
|
||||||
|
camera: string | null;
|
||||||
|
/**
|
||||||
|
* The maker of the camera
|
||||||
|
*/
|
||||||
|
maker: string | null;
|
||||||
|
/**
|
||||||
|
* The name of the lens
|
||||||
|
*/
|
||||||
|
lens: string | null;
|
||||||
|
dateShot: any | null;
|
||||||
|
/**
|
||||||
|
* The exposure time of the image
|
||||||
|
*/
|
||||||
|
exposure: number | null;
|
||||||
|
/**
|
||||||
|
* The aperature stops of the image
|
||||||
|
*/
|
||||||
|
aperture: number | null;
|
||||||
|
/**
|
||||||
|
* The ISO setting of the image
|
||||||
|
*/
|
||||||
|
iso: number | null;
|
||||||
|
/**
|
||||||
|
* The focal length of the lens, when the image was taken
|
||||||
|
*/
|
||||||
|
focalLength: number | null;
|
||||||
|
/**
|
||||||
|
* A formatted description of the flash settings, when the image was taken
|
||||||
|
*/
|
||||||
|
flash: number | null;
|
||||||
|
/**
|
||||||
|
* An index describing the mode for adjusting the exposure of the image
|
||||||
|
*/
|
||||||
|
exposureProgram: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface shareAlbumQuery_album_media {
|
||||||
|
__typename: "Media";
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
type: MediaType;
|
||||||
|
/**
|
||||||
|
* URL to display the media in a smaller resolution
|
||||||
|
*/
|
||||||
|
thumbnail: shareAlbumQuery_album_media_thumbnail | null;
|
||||||
|
downloads: shareAlbumQuery_album_media_downloads[];
|
||||||
|
/**
|
||||||
|
* URL to display the photo in full resolution, will be null for videos
|
||||||
|
*/
|
||||||
|
highRes: shareAlbumQuery_album_media_highRes | null;
|
||||||
|
/**
|
||||||
|
* URL to get the video in a web format that can be played in the browser, will be null for photos
|
||||||
|
*/
|
||||||
|
videoWeb: shareAlbumQuery_album_media_videoWeb | null;
|
||||||
|
exif: shareAlbumQuery_album_media_exif | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface shareAlbumQuery_album {
|
||||||
|
__typename: "Album";
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* The albums contained in this album
|
||||||
|
*/
|
||||||
|
subAlbums: shareAlbumQuery_album_subAlbums[];
|
||||||
|
/**
|
||||||
|
* The media inside this album
|
||||||
|
*/
|
||||||
|
media: shareAlbumQuery_album_media[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface shareAlbumQuery {
|
||||||
|
/**
|
||||||
|
* Get album by id, user must own the album or be admin
|
||||||
|
* If valid tokenCredentials are provided, the album may be retrived without further authentication
|
||||||
|
*/
|
||||||
|
album: shareAlbumQuery_album;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface shareAlbumQueryVariables {
|
||||||
|
id: string;
|
||||||
|
token: string;
|
||||||
|
password?: string | null;
|
||||||
|
limit?: number | null;
|
||||||
|
offset?: number | null;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: adminQuery
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface adminQuery_myUser {
|
||||||
|
__typename: "User";
|
||||||
|
admin: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface adminQuery {
|
||||||
|
/**
|
||||||
|
* Information about the currently logged in user
|
||||||
|
*/
|
||||||
|
myUser: adminQuery_myUser;
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: mapboxEnabledQuery
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface mapboxEnabledQuery {
|
||||||
|
/**
|
||||||
|
* Get the mapbox api token, returns null if mapbox is not enabled
|
||||||
|
*/
|
||||||
|
mapboxToken: string | null;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { LanguageTranslation } from "./../../__generated__/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: siteTranslation
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface siteTranslation_myUserPreferences {
|
||||||
|
__typename: "UserPreferences";
|
||||||
|
id: string;
|
||||||
|
language: LanguageTranslation | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface siteTranslation {
|
||||||
|
myUserPreferences: siteTranslation_myUserPreferences;
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ import {
|
||||||
split,
|
split,
|
||||||
ApolloLink,
|
ApolloLink,
|
||||||
HttpLink,
|
HttpLink,
|
||||||
|
ServerError,
|
||||||
|
FieldMergeFunction,
|
||||||
} from '@apollo/client'
|
} from '@apollo/client'
|
||||||
import { getMainDefinition } from '@apollo/client/utilities'
|
import { getMainDefinition } from '@apollo/client/utilities'
|
||||||
import { onError } from '@apollo/client/link/error'
|
import { onError } from '@apollo/client/link/error'
|
||||||
|
@ -12,6 +14,7 @@ import { WebSocketLink } from '@apollo/client/link/ws'
|
||||||
import urlJoin from 'url-join'
|
import urlJoin from 'url-join'
|
||||||
import { clearTokenCookie } from './helpers/authentication'
|
import { clearTokenCookie } from './helpers/authentication'
|
||||||
import { MessageState } from './components/messages/Messages'
|
import { MessageState } from './components/messages/Messages'
|
||||||
|
import { Message } from './components/messages/SubscriptionsHook'
|
||||||
|
|
||||||
export const GRAPHQL_ENDPOINT = process.env.PHOTOVIEW_API_ENDPOINT
|
export const GRAPHQL_ENDPOINT = process.env.PHOTOVIEW_API_ENDPOINT
|
||||||
? urlJoin(process.env.PHOTOVIEW_API_ENDPOINT, '/graphql')
|
? urlJoin(process.env.PHOTOVIEW_API_ENDPOINT, '/graphql')
|
||||||
|
@ -26,12 +29,12 @@ console.log('GRAPHQL ENDPOINT', GRAPHQL_ENDPOINT)
|
||||||
|
|
||||||
const apiProtocol = new URL(GRAPHQL_ENDPOINT).protocol
|
const apiProtocol = new URL(GRAPHQL_ENDPOINT).protocol
|
||||||
|
|
||||||
let websocketUri = new URL(GRAPHQL_ENDPOINT)
|
const websocketUri = new URL(GRAPHQL_ENDPOINT)
|
||||||
websocketUri.protocol = apiProtocol === 'https:' ? 'wss:' : 'ws:'
|
websocketUri.protocol = apiProtocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
|
|
||||||
const wsLink = new WebSocketLink({
|
const wsLink = new WebSocketLink({
|
||||||
uri: websocketUri,
|
uri: websocketUri.toString(),
|
||||||
credentials: 'include',
|
// credentials: 'include',
|
||||||
})
|
})
|
||||||
|
|
||||||
const link = split(
|
const link = split(
|
||||||
|
@ -48,7 +51,7 @@ const link = split(
|
||||||
)
|
)
|
||||||
|
|
||||||
const linkError = onError(({ graphQLErrors, networkError }) => {
|
const linkError = onError(({ graphQLErrors, networkError }) => {
|
||||||
let errorMessages = []
|
const errorMessages = []
|
||||||
|
|
||||||
if (graphQLErrors) {
|
if (graphQLErrors) {
|
||||||
graphQLErrors.map(({ message, locations, path }) =>
|
graphQLErrors.map(({ message, locations, path }) =>
|
||||||
|
@ -82,7 +85,7 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
|
||||||
console.log(`[Network error]: ${JSON.stringify(networkError)}`)
|
console.log(`[Network error]: ${JSON.stringify(networkError)}`)
|
||||||
clearTokenCookie()
|
clearTokenCookie()
|
||||||
|
|
||||||
const errors = networkError.result.errors
|
const errors = (networkError as ServerError).result.errors
|
||||||
|
|
||||||
if (errors.length == 1) {
|
if (errors.length == 1) {
|
||||||
errorMessages.push({
|
errorMessages.push({
|
||||||
|
@ -92,7 +95,9 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
|
||||||
} else if (errors.length > 1) {
|
} else if (errors.length > 1) {
|
||||||
errorMessages.push({
|
errorMessages.push({
|
||||||
header: 'Multiple server errors',
|
header: 'Multiple server errors',
|
||||||
content: `Received ${graphQLErrors.length} errors from the server. You are being logged out in an attempt to recover.`,
|
content: `Received ${
|
||||||
|
graphQLErrors?.length || 0
|
||||||
|
} errors from the server. You are being logged out in an attempt to recover.`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,26 +111,32 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
|
||||||
...msg,
|
...msg,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
MessageState.set(messages => [...messages, ...newMessages])
|
MessageState.set((messages: Message[]) => [...messages, ...newMessages])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
type PaginateCacheType = {
|
||||||
|
keyArgs: string[]
|
||||||
|
merge: FieldMergeFunction
|
||||||
|
}
|
||||||
|
|
||||||
// Modified version of Apollo's offsetLimitPagination()
|
// Modified version of Apollo's offsetLimitPagination()
|
||||||
const paginateCache = keyArgs => ({
|
const paginateCache = (keyArgs: string[]) =>
|
||||||
keyArgs,
|
({
|
||||||
merge(existing, incoming, { args, fieldName }) {
|
keyArgs,
|
||||||
const merged = existing ? existing.slice(0) : []
|
merge(existing, incoming, { args, fieldName }) {
|
||||||
if (args?.paginate) {
|
const merged = existing ? existing.slice(0) : []
|
||||||
const { offset = 0 } = args.paginate
|
if (args?.paginate) {
|
||||||
for (let i = 0; i < incoming.length; ++i) {
|
const { offset = 0 } = args.paginate
|
||||||
merged[offset + i] = incoming[i]
|
for (let i = 0; i < incoming.length; ++i) {
|
||||||
|
merged[offset + i] = incoming[i]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Paginate argument is missing for query: ${fieldName}`)
|
||||||
}
|
}
|
||||||
} else {
|
return merged
|
||||||
throw new Error(`Paginate argument is missing for query: ${fieldName}`)
|
},
|
||||||
}
|
} as PaginateCacheType)
|
||||||
return merged
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const memoryCache = new InMemoryCache({
|
const memoryCache = new InMemoryCache({
|
||||||
typePolicies: {
|
typePolicies: {
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useContext } from 'react'
|
import React, { useEffect, useContext } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Breadcrumb } from 'semantic-ui-react'
|
import { Breadcrumb, IconProps } from 'semantic-ui-react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { Icon } from 'semantic-ui-react'
|
import { Icon } from 'semantic-ui-react'
|
||||||
|
@ -8,6 +8,7 @@ import { SidebarContext } from './sidebar/Sidebar'
|
||||||
import AlbumSidebar from './sidebar/AlbumSidebar'
|
import AlbumSidebar from './sidebar/AlbumSidebar'
|
||||||
import { useLazyQuery, gql } from '@apollo/client'
|
import { useLazyQuery, gql } from '@apollo/client'
|
||||||
import { authToken } from '../helpers/authentication'
|
import { authToken } from '../helpers/authentication'
|
||||||
|
import { albumPathQuery } from './__generated__/albumPathQuery'
|
||||||
|
|
||||||
const Header = styled.h1`
|
const Header = styled.h1`
|
||||||
margin: 24px 0 8px 0 !important;
|
margin: 24px 0 8px 0 !important;
|
||||||
|
@ -32,7 +33,7 @@ const StyledIcon = styled(Icon)`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const SettingsIcon = props => {
|
const SettingsIcon = (props: IconProps) => {
|
||||||
return <StyledIcon name="settings" size="small" {...props} />
|
return <StyledIcon name="settings" size="small" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,8 +49,18 @@ const ALBUM_PATH_QUERY = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const AlbumTitle = ({ album, disableLink = false }) => {
|
type AlbumTitleProps = {
|
||||||
const [fetchPath, { data: pathData }] = useLazyQuery(ALBUM_PATH_QUERY)
|
album?: {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
disableLink: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlbumTitle = ({ album, disableLink = false }: AlbumTitleProps) => {
|
||||||
|
const [fetchPath, { data: pathData }] = useLazyQuery<albumPathQuery>(
|
||||||
|
ALBUM_PATH_QUERY
|
||||||
|
)
|
||||||
const { updateSidebar } = useContext(SidebarContext)
|
const { updateSidebar } = useContext(SidebarContext)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -68,10 +79,7 @@ const AlbumTitle = ({ album, disableLink = false }) => {
|
||||||
|
|
||||||
let title = <span>{album.title}</span>
|
let title = <span>{album.title}</span>
|
||||||
|
|
||||||
let path = []
|
const path = pathData?.album.path || []
|
||||||
if (pathData) {
|
|
||||||
path = pathData.album.path
|
|
||||||
}
|
|
||||||
|
|
||||||
const breadcrumbSections = path
|
const breadcrumbSections = path
|
||||||
.slice()
|
.slice()
|
|
@ -1,8 +1,12 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { Loader } from 'semantic-ui-react'
|
import { Loader } from 'semantic-ui-react'
|
||||||
|
|
||||||
const PaginateLoader = ({ active, text }) => (
|
type PaginateLoaderProps = {
|
||||||
|
active: boolean
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaginateLoader = ({ active, text }: PaginateLoaderProps) => (
|
||||||
<Loader
|
<Loader
|
||||||
style={{ margin: '42px 0 24px 0', opacity: active ? '1' : '0' }}
|
style={{ margin: '42px 0 24px 0', opacity: active ? '1' : '0' }}
|
||||||
inline="centered"
|
inline="centered"
|
||||||
|
@ -12,9 +16,4 @@ const PaginateLoader = ({ active, text }) => (
|
||||||
</Loader>
|
</Loader>
|
||||||
)
|
)
|
||||||
|
|
||||||
PaginateLoader.propTypes = {
|
|
||||||
active: PropTypes.bool,
|
|
||||||
text: PropTypes.string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PaginateLoader
|
export default PaginateLoader
|
|
@ -0,0 +1,32 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: albumPathQuery
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface albumPathQuery_album_path {
|
||||||
|
__typename: "Album";
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface albumPathQuery_album {
|
||||||
|
__typename: "Album";
|
||||||
|
id: string;
|
||||||
|
path: albumPathQuery_album_path[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface albumPathQuery {
|
||||||
|
/**
|
||||||
|
* Get album by id, user must own the album or be admin
|
||||||
|
* If valid tokenCredentials are provided, the album may be retrived without further authentication
|
||||||
|
*/
|
||||||
|
album: albumPathQuery_album;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface albumPathQueryVariables {
|
||||||
|
id: string;
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ProtectedImage } from '../photoGallery/ProtectedMedia'
|
import { ProtectedImage } from '../photoGallery/ProtectedMedia'
|
||||||
|
import { albumQuery_album_subAlbums } from '../../Pages/AlbumPage/__generated__/albumQuery'
|
||||||
|
|
||||||
const AlbumBoxLink = styled(Link)`
|
const AlbumBoxLink = styled(Link)`
|
||||||
width: 240px;
|
width: 240px;
|
||||||
|
@ -28,7 +28,7 @@ const Image = styled(ProtectedImage)`
|
||||||
object-position: center;
|
object-position: center;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Placeholder = styled.div`
|
const Placeholder = styled.div<{ overlap?: boolean; loaded?: boolean }>`
|
||||||
width: 220px;
|
width: 220px;
|
||||||
height: 220px;
|
height: 220px;
|
||||||
border-radius: 4%;
|
border-radius: 4%;
|
||||||
|
@ -47,14 +47,18 @@ const Placeholder = styled.div`
|
||||||
`}
|
`}
|
||||||
`
|
`
|
||||||
|
|
||||||
const AlbumBoxImage = ({ src, ...props }) => {
|
interface AlbumBoxImageProps {
|
||||||
|
src?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlbumBoxImage = ({ src, ...props }: AlbumBoxImageProps) => {
|
||||||
const [loaded, setLoaded] = useState(false)
|
const [loaded, setLoaded] = useState(false)
|
||||||
|
|
||||||
if (src) {
|
if (src) {
|
||||||
return (
|
return (
|
||||||
<ImageWrapper>
|
<ImageWrapper>
|
||||||
<Image {...props} onLoad={loaded => setLoaded(loaded)} src={src} />
|
<Image {...props} onLoad={() => setLoaded(true)} src={src} />
|
||||||
<Placeholder overlap loaded={loaded ? 1 : 0} />
|
<Placeholder overlap loaded={loaded} />
|
||||||
</ImageWrapper>
|
</ImageWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -62,11 +66,12 @@ const AlbumBoxImage = ({ src, ...props }) => {
|
||||||
return <Placeholder />
|
return <Placeholder />
|
||||||
}
|
}
|
||||||
|
|
||||||
AlbumBoxImage.propTypes = {
|
type AlbumBoxProps = {
|
||||||
src: PropTypes.string,
|
album?: albumQuery_album_subAlbums
|
||||||
|
customLink?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlbumBox = ({ album, customLink, ...props }) => {
|
export const AlbumBox = ({ album, customLink, ...props }: AlbumBoxProps) => {
|
||||||
if (!album) {
|
if (!album) {
|
||||||
return (
|
return (
|
||||||
<AlbumBoxLink {...props} to="#">
|
<AlbumBoxLink {...props} to="#">
|
||||||
|
@ -75,7 +80,7 @@ export const AlbumBox = ({ album, customLink, ...props }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let thumbnail = album.thumbnail?.thumbnail?.url
|
const thumbnail = album.thumbnail?.thumbnail?.url
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlbumBoxLink {...props} to={customLink || `/album/${album.id}`}>
|
<AlbumBoxLink {...props} to={customLink || `/album/${album.id}`}>
|
||||||
|
@ -84,8 +89,3 @@ export const AlbumBox = ({ album, customLink, ...props }) => {
|
||||||
</AlbumBoxLink>
|
</AlbumBoxLink>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AlbumBox.propTypes = {
|
|
||||||
album: PropTypes.object,
|
|
||||||
customLink: PropTypes.string,
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
import { albumQuery_album_subAlbums } from '../../Pages/AlbumPage/__generated__/albumQuery'
|
||||||
import { AlbumBox } from './AlbumBox'
|
import { AlbumBox } from './AlbumBox'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
|
@ -8,7 +8,14 @@ const Container = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
`
|
`
|
||||||
|
|
||||||
const AlbumBoxes = ({ error, albums, getCustomLink }) => {
|
type AlbumBoxesProps = {
|
||||||
|
loading: boolean
|
||||||
|
error?: Error
|
||||||
|
albums?: albumQuery_album_subAlbums[]
|
||||||
|
getCustomLink?(albumID: string): string
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlbumBoxes = ({ error, albums, getCustomLink }: AlbumBoxesProps) => {
|
||||||
if (error) return <div>Error {error.message}</div>
|
if (error) return <div>Error {error.message}</div>
|
||||||
|
|
||||||
let albumElements = []
|
let albumElements = []
|
||||||
|
@ -18,7 +25,7 @@ const AlbumBoxes = ({ error, albums, getCustomLink }) => {
|
||||||
<AlbumBox
|
<AlbumBox
|
||||||
key={album.id}
|
key={album.id}
|
||||||
album={album}
|
album={album}
|
||||||
customLink={getCustomLink ? getCustomLink(album.id) : null}
|
customLink={getCustomLink ? getCustomLink(album.id) : undefined}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
|
@ -30,11 +37,4 @@ const AlbumBoxes = ({ error, albums, getCustomLink }) => {
|
||||||
return <Container>{albumElements}</Container>
|
return <Container>{albumElements}</Container>
|
||||||
}
|
}
|
||||||
|
|
||||||
AlbumBoxes.propTypes = {
|
|
||||||
loading: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
albums: PropTypes.array,
|
|
||||||
getCustomLink: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AlbumBoxes
|
export default AlbumBoxes
|
|
@ -1,9 +1,22 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import AlbumTitle from '../AlbumTitle'
|
import AlbumTitle from '../AlbumTitle'
|
||||||
import PhotoGallery from '../photoGallery/PhotoGallery'
|
import PhotoGallery from '../photoGallery/PhotoGallery'
|
||||||
import AlbumBoxes from './AlbumBoxes'
|
import AlbumBoxes from './AlbumBoxes'
|
||||||
import AlbumFilter from '../AlbumFilter'
|
import AlbumFilter from '../AlbumFilter'
|
||||||
|
import { albumQuery_album } from '../../Pages/AlbumPage/__generated__/albumQuery'
|
||||||
|
import { OrderDirection } from '../../../__generated__/globalTypes'
|
||||||
|
|
||||||
|
type AlbumGalleryProps = {
|
||||||
|
album?: albumQuery_album
|
||||||
|
loading?: boolean
|
||||||
|
customAlbumLink?(albumID: string): string
|
||||||
|
showFilter?: boolean
|
||||||
|
setOnlyFavorites?(favorites: boolean): void
|
||||||
|
setOrdering?(ordering: { orderBy: string }): void
|
||||||
|
ordering?: { orderBy: string | null; orderDirection: OrderDirection | null }
|
||||||
|
onlyFavorites?: boolean
|
||||||
|
onFavorite?(): void
|
||||||
|
}
|
||||||
|
|
||||||
const AlbumGallery = React.forwardRef(
|
const AlbumGallery = React.forwardRef(
|
||||||
(
|
(
|
||||||
|
@ -17,18 +30,23 @@ const AlbumGallery = React.forwardRef(
|
||||||
ordering,
|
ordering,
|
||||||
onlyFavorites = false,
|
onlyFavorites = false,
|
||||||
onFavorite,
|
onFavorite,
|
||||||
},
|
}: AlbumGalleryProps,
|
||||||
ref
|
ref: React.ForwardedRef<HTMLDivElement>
|
||||||
) => {
|
) => {
|
||||||
const [imageState, setImageState] = useState({
|
type ImageStateType = {
|
||||||
|
activeImage: number
|
||||||
|
presenting: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const [imageState, setImageState] = useState<ImageStateType>({
|
||||||
activeImage: -1,
|
activeImage: -1,
|
||||||
presenting: false,
|
presenting: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const setPresenting = presenting =>
|
const setPresenting = (presenting: boolean) =>
|
||||||
setImageState(state => ({ ...state, presenting }))
|
setImageState(state => ({ ...state, presenting }))
|
||||||
|
|
||||||
const setPresentingWithHistory = presenting => {
|
const setPresentingWithHistory = (presenting: boolean) => {
|
||||||
setPresenting(presenting)
|
setPresenting(presenting)
|
||||||
if (presenting) {
|
if (presenting) {
|
||||||
history.pushState({ imageState }, '')
|
history.pushState({ imageState }, '')
|
||||||
|
@ -37,20 +55,23 @@ const AlbumGallery = React.forwardRef(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateHistory = imageState => {
|
const updateHistory = (imageState: ImageStateType) => {
|
||||||
history.replaceState({ imageState }, '')
|
history.replaceState({ imageState }, '')
|
||||||
return imageState
|
return imageState
|
||||||
}
|
}
|
||||||
|
|
||||||
const setActiveImage = activeImage => {
|
const setActiveImage = (activeImage: number) => {
|
||||||
setImageState(state => updateHistory({ ...state, activeImage }))
|
setImageState(state => updateHistory({ ...state, activeImage }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextImage = () => {
|
const nextImage = () => {
|
||||||
|
if (album === undefined) return
|
||||||
setActiveImage((imageState.activeImage + 1) % album.media.length)
|
setActiveImage((imageState.activeImage + 1) % album.media.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousImage = () => {
|
const previousImage = () => {
|
||||||
|
if (album === undefined) return
|
||||||
|
|
||||||
if (imageState.activeImage <= 0) {
|
if (imageState.activeImage <= 0) {
|
||||||
setActiveImage(album.media.length - 1)
|
setActiveImage(album.media.length - 1)
|
||||||
} else {
|
} else {
|
||||||
|
@ -59,9 +80,10 @@ const AlbumGallery = React.forwardRef(
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateImageState = event => {
|
const updateImageState = (event: PopStateEvent) => {
|
||||||
setImageState(event.state.imageState)
|
setImageState(event.state.imageState)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('popstate', updateImageState)
|
window.addEventListener('popstate', updateImageState)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -113,7 +135,7 @@ const AlbumGallery = React.forwardRef(
|
||||||
}
|
}
|
||||||
<PhotoGallery
|
<PhotoGallery
|
||||||
loading={loading}
|
loading={loading}
|
||||||
media={album && album.media}
|
media={album?.media || []}
|
||||||
activeIndex={imageState.activeImage}
|
activeIndex={imageState.activeImage}
|
||||||
presenting={imageState.presenting}
|
presenting={imageState.presenting}
|
||||||
onSelectImage={index => {
|
onSelectImage={index => {
|
||||||
|
@ -129,16 +151,4 @@ const AlbumGallery = React.forwardRef(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
AlbumGallery.propTypes = {
|
|
||||||
album: PropTypes.object,
|
|
||||||
loading: PropTypes.bool,
|
|
||||||
customAlbumLink: PropTypes.func,
|
|
||||||
showFilter: PropTypes.bool,
|
|
||||||
setOnlyFavorites: PropTypes.func,
|
|
||||||
onlyFavorites: PropTypes.bool,
|
|
||||||
onFavorite: PropTypes.func,
|
|
||||||
setOrdering: PropTypes.func,
|
|
||||||
ordering: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AlbumGallery
|
export default AlbumGallery
|
|
@ -1,11 +1,15 @@
|
||||||
import React, { useState, useRef, useEffect } from 'react'
|
import React, { useState, useRef, useEffect } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { useLazyQuery, gql } from '@apollo/client'
|
import { useLazyQuery, gql } from '@apollo/client'
|
||||||
import { debounce } from '../../helpers/utils'
|
import { debounce, DebouncedFn } from '../../helpers/utils'
|
||||||
import { ProtectedImage } from '../photoGallery/ProtectedMedia'
|
import { ProtectedImage } from '../photoGallery/ProtectedMedia'
|
||||||
import { NavLink } from 'react-router-dom'
|
import { NavLink } from 'react-router-dom'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import {
|
||||||
|
searchQuery,
|
||||||
|
searchQuery_search_albums,
|
||||||
|
searchQuery_search_media,
|
||||||
|
} from './__generated__/searchQuery'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
height: 60px;
|
height: 60px;
|
||||||
|
@ -30,7 +34,7 @@ const SearchField = styled.input`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Results = styled.div`
|
const Results = styled.div<{ show: boolean }>`
|
||||||
display: ${({ show }) => (show ? 'block' : 'none')};
|
display: ${({ show }) => (show ? 'block' : 'none')};
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -79,27 +83,29 @@ const SEARCH_QUERY = gql`
|
||||||
|
|
||||||
const SearchBar = () => {
|
const SearchBar = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [fetchSearches, fetchResult] = useLazyQuery(SEARCH_QUERY)
|
const [fetchSearches, fetchResult] = useLazyQuery<searchQuery>(SEARCH_QUERY)
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const [fetched, setFetched] = useState(false)
|
const [fetched, setFetched] = useState(false)
|
||||||
|
|
||||||
let debouncedFetch = useRef(null)
|
type QueryFn = (query: string) => void
|
||||||
|
|
||||||
|
const debouncedFetch = useRef<null | DebouncedFn<QueryFn>>(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
debouncedFetch.current = debounce(query => {
|
debouncedFetch.current = debounce<QueryFn>(query => {
|
||||||
fetchSearches({ variables: { query } })
|
fetchSearches({ variables: { query } })
|
||||||
setFetched(true)
|
setFetched(true)
|
||||||
}, 250)
|
}, 250)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
debouncedFetch.current.cancel()
|
debouncedFetch.current?.cancel()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const fetchEvent = e => {
|
const fetchEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
e.persist()
|
e.persist()
|
||||||
|
|
||||||
setQuery(e.target.value)
|
setQuery(e.target.value)
|
||||||
if (e.target.value.trim() != '') {
|
if (e.target.value.trim() != '' && debouncedFetch.current) {
|
||||||
debouncedFetch.current(e.target.value.trim())
|
debouncedFetch.current(e.target.value.trim())
|
||||||
} else {
|
} else {
|
||||||
setFetched(false)
|
setFetched(false)
|
||||||
|
@ -108,7 +114,12 @@ const SearchBar = () => {
|
||||||
|
|
||||||
let results = null
|
let results = null
|
||||||
if (query.trim().length > 0 && fetched) {
|
if (query.trim().length > 0 && fetched) {
|
||||||
results = <SearchResults result={fetchResult} />
|
results = (
|
||||||
|
<SearchResults
|
||||||
|
searchData={fetchResult.data}
|
||||||
|
loading={fetchResult.loading}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -128,17 +139,21 @@ const ResultTitle = styled.h1`
|
||||||
margin: 12px 0 0.25rem;
|
margin: 12px 0 0.25rem;
|
||||||
`
|
`
|
||||||
|
|
||||||
const SearchResults = ({ result }) => {
|
type SearchResultsProps = {
|
||||||
const { t } = useTranslation()
|
searchData?: searchQuery
|
||||||
const { data, loading } = result
|
loading: boolean
|
||||||
const query = data && data.search.query
|
}
|
||||||
|
|
||||||
const media = (data && data.search.media) || []
|
const SearchResults = ({ searchData, loading }: SearchResultsProps) => {
|
||||||
const albums = (data && data.search.albums) || []
|
const { t } = useTranslation()
|
||||||
|
const query = searchData?.search.query || ''
|
||||||
|
|
||||||
|
const media = searchData?.search.media || []
|
||||||
|
const albums = searchData?.search.albums || []
|
||||||
|
|
||||||
let message = null
|
let message = null
|
||||||
if (loading) message = t('header.search.loading', 'Loading results...')
|
if (loading) message = t('header.search.loading', 'Loading results...')
|
||||||
else if (data && media.length == 0 && albums.length == 0)
|
else if (searchData && media.length == 0 && albums.length == 0)
|
||||||
message = t('header.search.no_results', 'No results found')
|
message = t('header.search.no_results', 'No results found')
|
||||||
|
|
||||||
const albumElements = albums.map(album => (
|
const albumElements = albums.map(album => (
|
||||||
|
@ -155,7 +170,7 @@ const SearchResults = ({ result }) => {
|
||||||
// Prevent input blur event
|
// Prevent input blur event
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}}
|
}}
|
||||||
show={data}
|
show={!!searchData}
|
||||||
>
|
>
|
||||||
{message}
|
{message}
|
||||||
{albumElements.length > 0 && (
|
{albumElements.length > 0 && (
|
||||||
|
@ -174,10 +189,6 @@ const SearchResults = ({ result }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchResults.propTypes = {
|
|
||||||
result: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
const RowLink = styled(NavLink)`
|
const RowLink = styled(NavLink)`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -205,31 +216,31 @@ const RowTitle = styled.span`
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const PhotoRow = ({ query, media }) => (
|
type PhotoRowArgs = {
|
||||||
|
query: string
|
||||||
|
media: searchQuery_search_media
|
||||||
|
}
|
||||||
|
|
||||||
|
const PhotoRow = ({ query, media }: PhotoRowArgs) => (
|
||||||
<RowLink to={`/album/${media.album.id}`}>
|
<RowLink to={`/album/${media.album.id}`}>
|
||||||
<PhotoSearchThumbnail src={media?.thumbnail?.url} />
|
<PhotoSearchThumbnail src={media?.thumbnail?.url} />
|
||||||
<RowTitle>{searchHighlighted(query, media.title)}</RowTitle>
|
<RowTitle>{searchHighlighted(query, media.title)}</RowTitle>
|
||||||
</RowLink>
|
</RowLink>
|
||||||
)
|
)
|
||||||
|
|
||||||
PhotoRow.propTypes = {
|
type AlbumRowArgs = {
|
||||||
query: PropTypes.string.isRequired,
|
query: string
|
||||||
media: PropTypes.object.isRequired,
|
album: searchQuery_search_albums
|
||||||
}
|
}
|
||||||
|
|
||||||
const AlbumRow = ({ query, album }) => (
|
const AlbumRow = ({ query, album }: AlbumRowArgs) => (
|
||||||
<RowLink to={`/album/${album.id}`}>
|
<RowLink to={`/album/${album.id}`}>
|
||||||
<AlbumSearchThumbnail src={album?.thumbnail?.thumbnail?.url} />
|
<AlbumSearchThumbnail src={album?.thumbnail?.thumbnail?.url} />
|
||||||
<RowTitle>{searchHighlighted(query, album.title)}</RowTitle>
|
<RowTitle>{searchHighlighted(query, album.title)}</RowTitle>
|
||||||
</RowLink>
|
</RowLink>
|
||||||
)
|
)
|
||||||
|
|
||||||
AlbumRow.propTypes = {
|
const searchHighlighted = (query: string, text: string) => {
|
||||||
query: PropTypes.string.isRequired,
|
|
||||||
album: PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchHighlighted = (query, text) => {
|
|
||||||
const i = text.toLowerCase().indexOf(query.toLowerCase())
|
const i = text.toLowerCase().indexOf(query.toLowerCase())
|
||||||
|
|
||||||
if (i == -1) {
|
if (i == -1) {
|
|
@ -0,0 +1,76 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: searchQuery
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface searchQuery_search_albums_thumbnail_thumbnail {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface searchQuery_search_albums_thumbnail {
|
||||||
|
__typename: "Media";
|
||||||
|
/**
|
||||||
|
* URL to display the media in a smaller resolution
|
||||||
|
*/
|
||||||
|
thumbnail: searchQuery_search_albums_thumbnail_thumbnail | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface searchQuery_search_albums {
|
||||||
|
__typename: "Album";
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* An image in this album used for previewing this album
|
||||||
|
*/
|
||||||
|
thumbnail: searchQuery_search_albums_thumbnail | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface searchQuery_search_media_thumbnail {
|
||||||
|
__typename: "MediaURL";
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface searchQuery_search_media_album {
|
||||||
|
__typename: "Album";
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface searchQuery_search_media {
|
||||||
|
__typename: "Media";
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* URL to display the media in a smaller resolution
|
||||||
|
*/
|
||||||
|
thumbnail: searchQuery_search_media_thumbnail | null;
|
||||||
|
/**
|
||||||
|
* The album that holds the media
|
||||||
|
*/
|
||||||
|
album: searchQuery_search_media_album;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface searchQuery_search {
|
||||||
|
__typename: "SearchResult";
|
||||||
|
query: string;
|
||||||
|
albums: searchQuery_search_albums[];
|
||||||
|
media: searchQuery_search_media[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface searchQuery {
|
||||||
|
search: searchQuery_search;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface searchQueryVariables {
|
||||||
|
query: string;
|
||||||
|
}
|
|
@ -17,9 +17,11 @@ const Container = styled.div`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export let MessageState = {
|
export const MessageState = {
|
||||||
set: null,
|
set: fn => {
|
||||||
get: null,
|
console.warn('set function is not defined yet, called with', fn)
|
||||||
|
},
|
||||||
|
get: [],
|
||||||
add: message => {
|
add: message => {
|
||||||
MessageState.set(messages => {
|
MessageState.set(messages => {
|
||||||
const newMessages = messages.filter(msg => msg.key != message.key)
|
const newMessages = messages.filter(msg => msg.key != message.key)
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
import { notificationSubscription } from './__generated__/notificationSubscription'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useSubscription, gql } from '@apollo/client'
|
import { useSubscription, gql } from '@apollo/client'
|
||||||
import { authToken } from '../../helpers/authentication'
|
import { authToken } from '../../helpers/authentication'
|
||||||
|
import { NotificationType } from '../../../__generated__/globalTypes'
|
||||||
|
|
||||||
const notificationSubscription = gql`
|
const NOTIFICATION_SUBSCRIPTION = gql`
|
||||||
subscription notificationSubscription {
|
subscription notificationSubscription {
|
||||||
notification {
|
notification {
|
||||||
key
|
key
|
||||||
|
@ -18,14 +20,37 @@ const notificationSubscription = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
let messageTimeoutHandles = new Map()
|
const messageTimeoutHandles = new Map()
|
||||||
|
|
||||||
const SubscriptionsHook = ({ messages, setMessages }) => {
|
export interface Message {
|
||||||
|
key: string
|
||||||
|
type: NotificationType
|
||||||
|
timeout?: number
|
||||||
|
props: {
|
||||||
|
header: string
|
||||||
|
content: string
|
||||||
|
negative?: boolean
|
||||||
|
positive?: boolean
|
||||||
|
percent?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscriptionHookProps = {
|
||||||
|
messages: Message[]
|
||||||
|
setMessages: React.Dispatch<React.SetStateAction<Message[]>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const SubscriptionsHook = ({
|
||||||
|
messages,
|
||||||
|
setMessages,
|
||||||
|
}: SubscriptionHookProps) => {
|
||||||
if (!authToken()) {
|
if (!authToken()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = useSubscription(notificationSubscription)
|
const { data, error } = useSubscription<notificationSubscription>(
|
||||||
|
NOTIFICATION_SUBSCRIPTION
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -33,7 +58,7 @@ const SubscriptionsHook = ({ messages, setMessages }) => {
|
||||||
...state,
|
...state,
|
||||||
{
|
{
|
||||||
key: Math.random().toString(26),
|
key: Math.random().toString(26),
|
||||||
type: 'message',
|
type: NotificationType.Message,
|
||||||
props: {
|
props: {
|
||||||
header: 'Network error',
|
header: 'Network error',
|
||||||
content: error.message,
|
content: error.message,
|
||||||
|
@ -54,16 +79,16 @@ const SubscriptionsHook = ({ messages, setMessages }) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newNotification = {
|
const newNotification: Message = {
|
||||||
key: msg.key,
|
key: msg.key,
|
||||||
type: msg.type.toLowerCase(),
|
type: msg.type,
|
||||||
timeout: msg.timeout,
|
timeout: msg.timeout || undefined,
|
||||||
props: {
|
props: {
|
||||||
header: msg.header,
|
header: msg.header,
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
negative: msg.negative,
|
negative: msg.negative,
|
||||||
positive: msg.positive,
|
positive: msg.positive,
|
||||||
percent: msg.progress,
|
percent: msg.progress || undefined,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { NotificationType } from "./../../../../__generated__/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL subscription operation: notificationSubscription
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface notificationSubscription_notification {
|
||||||
|
__typename: "Notification";
|
||||||
|
key: string;
|
||||||
|
type: NotificationType;
|
||||||
|
header: string;
|
||||||
|
content: string;
|
||||||
|
progress: number | null;
|
||||||
|
positive: boolean;
|
||||||
|
negative: boolean;
|
||||||
|
/**
|
||||||
|
* Time in milliseconds before the notification will close
|
||||||
|
*/
|
||||||
|
timeout: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface notificationSubscription {
|
||||||
|
notification: notificationSubscription_notification;
|
||||||
|
}
|
|
@ -1,9 +1,13 @@
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { useMutation, gql } from '@apollo/client'
|
import { useMutation, gql } from '@apollo/client'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { Icon } from 'semantic-ui-react'
|
import { Icon } from 'semantic-ui-react'
|
||||||
import { ProtectedImage } from './ProtectedMedia'
|
import { ProtectedImage } from './ProtectedMedia'
|
||||||
|
import { MediaType } from '../../../__generated__/globalTypes'
|
||||||
|
import {
|
||||||
|
markMediaFavorite,
|
||||||
|
markMediaFavoriteVariables,
|
||||||
|
} from './__generated__/markMediaFavorite'
|
||||||
|
|
||||||
const markFavoriteMutation = gql`
|
const markFavoriteMutation = gql`
|
||||||
mutation markMediaFavorite($mediaId: ID!, $favorite: Boolean!) {
|
mutation markMediaFavorite($mediaId: ID!, $favorite: Boolean!) {
|
||||||
|
@ -24,7 +28,7 @@ const MediaContainer = styled.div`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`
|
`
|
||||||
|
|
||||||
const StyledPhoto = styled(ProtectedImage)`
|
const StyledPhoto = styled(ProtectedImage)<{ loaded: boolean }>`
|
||||||
height: 200px;
|
height: 200px;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -34,35 +38,30 @@ const StyledPhoto = styled(ProtectedImage)`
|
||||||
transition: opacity 300ms;
|
transition: opacity 300ms;
|
||||||
`
|
`
|
||||||
|
|
||||||
const LazyPhoto = photoProps => {
|
type LazyPhotoProps = {
|
||||||
|
src?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const LazyPhoto = (photoProps: LazyPhotoProps) => {
|
||||||
const [loaded, setLoaded] = useState(false)
|
const [loaded, setLoaded] = useState(false)
|
||||||
const onLoad = useCallback(e => {
|
const onLoad = useCallback(e => {
|
||||||
!e.target.dataset.src && setLoaded(true)
|
!e.target.dataset.src && setLoaded(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledPhoto
|
<StyledPhoto {...photoProps} lazyLoading loaded={loaded} onLoad={onLoad} />
|
||||||
{...photoProps}
|
|
||||||
lazyLoading
|
|
||||||
loaded={loaded ? 1 : 0}
|
|
||||||
onLoad={onLoad}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyPhoto.propTypes = {
|
const PhotoOverlay = styled.div<{ active: boolean }>`
|
||||||
src: PropTypes.string,
|
|
||||||
}
|
|
||||||
|
|
||||||
const PhotoOverlay = styled.div`
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
${props =>
|
${({ active }) =>
|
||||||
props.active &&
|
active &&
|
||||||
`
|
`
|
||||||
border: 4px solid rgba(65, 131, 196, 0.6);
|
border: 4px solid rgba(65, 131, 196, 0.6);
|
||||||
|
|
||||||
|
@ -109,6 +108,24 @@ const VideoThumbnailIcon = styled(Icon)`
|
||||||
top: calc(50% - 13px);
|
top: calc(50% - 13px);
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type MediaThumbnailProps = {
|
||||||
|
media: {
|
||||||
|
id: string
|
||||||
|
type: MediaType
|
||||||
|
favorite?: boolean
|
||||||
|
thumbnail: null | {
|
||||||
|
url: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSelectImage(index: number): void
|
||||||
|
index: number
|
||||||
|
active: boolean
|
||||||
|
setPresenting(presenting: boolean): void
|
||||||
|
onFavorite?(): void
|
||||||
|
}
|
||||||
|
|
||||||
export const MediaThumbnail = ({
|
export const MediaThumbnail = ({
|
||||||
media,
|
media,
|
||||||
onSelectImage,
|
onSelectImage,
|
||||||
|
@ -116,16 +133,19 @@ export const MediaThumbnail = ({
|
||||||
active,
|
active,
|
||||||
setPresenting,
|
setPresenting,
|
||||||
onFavorite,
|
onFavorite,
|
||||||
}) => {
|
}: MediaThumbnailProps) => {
|
||||||
const [markFavorite] = useMutation(markFavoriteMutation)
|
const [markFavorite] = useMutation<
|
||||||
|
markMediaFavorite,
|
||||||
|
markMediaFavoriteVariables
|
||||||
|
>(markFavoriteMutation)
|
||||||
|
|
||||||
let heartIcon = null
|
let heartIcon = null
|
||||||
if (typeof media.favorite == 'boolean') {
|
if (media.favorite !== undefined) {
|
||||||
heartIcon = (
|
heartIcon = (
|
||||||
<FavoriteIcon
|
<FavoriteIcon
|
||||||
favorite={media.favorite.toString()}
|
favorite={media.favorite.toString()}
|
||||||
name={media.favorite ? 'heart' : 'heart outline'}
|
name={media.favorite ? 'heart' : 'heart outline'}
|
||||||
onClick={event => {
|
onClick={(event: MouseEvent) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
const favorite = !media.favorite
|
const favorite = !media.favorite
|
||||||
markFavorite({
|
markFavorite({
|
||||||
|
@ -148,7 +168,7 @@ export const MediaThumbnail = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
let videoIcon = null
|
let videoIcon = null
|
||||||
if (media.type == 'video') {
|
if (media.type == MediaType.Video) {
|
||||||
videoIcon = <VideoThumbnailIcon name="play" size="big" />
|
videoIcon = <VideoThumbnailIcon name="play" size="big" />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,11 +183,11 @@ export const MediaThumbnail = ({
|
||||||
<MediaContainer
|
<MediaContainer
|
||||||
key={media.id}
|
key={media.id}
|
||||||
style={{
|
style={{
|
||||||
cursor: onSelectImage ? 'pointer' : null,
|
cursor: 'pointer',
|
||||||
minWidth: `clamp(124px, ${minWidth}px, 100% - 8px)`,
|
minWidth: `clamp(124px, ${minWidth}px, 100% - 8px)`,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSelectImage && onSelectImage(index)
|
onSelectImage(index)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -176,7 +196,7 @@ export const MediaThumbnail = ({
|
||||||
height: `200px`,
|
height: `200px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LazyPhoto src={media.thumbnail && media.thumbnail.url} />
|
<LazyPhoto src={media.thumbnail?.url} />
|
||||||
</div>
|
</div>
|
||||||
<PhotoOverlay active={active}>
|
<PhotoOverlay active={active}>
|
||||||
{videoIcon}
|
{videoIcon}
|
||||||
|
@ -192,15 +212,6 @@ export const MediaThumbnail = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaThumbnail.propTypes = {
|
|
||||||
media: PropTypes.object.isRequired,
|
|
||||||
onSelectImage: PropTypes.func,
|
|
||||||
index: PropTypes.number.isRequired,
|
|
||||||
active: PropTypes.bool.isRequired,
|
|
||||||
setPresenting: PropTypes.func.isRequired,
|
|
||||||
onFavorite: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PhotoThumbnail = styled.div`
|
export const PhotoThumbnail = styled.div`
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
height: 200px;
|
height: 200px;
|
|
@ -3,10 +3,11 @@ import styled from 'styled-components'
|
||||||
import { Loader } from 'semantic-ui-react'
|
import { Loader } from 'semantic-ui-react'
|
||||||
import { MediaThumbnail, PhotoThumbnail } from './MediaThumbnail'
|
import { MediaThumbnail, PhotoThumbnail } from './MediaThumbnail'
|
||||||
import PresentView from './presentView/PresentView'
|
import PresentView from './presentView/PresentView'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { SidebarContext } from '../sidebar/Sidebar'
|
import { SidebarContext } from '../sidebar/Sidebar'
|
||||||
import MediaSidebar from '../sidebar/MediaSidebar'
|
import MediaSidebar from '../sidebar/MediaSidebar'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { PresentMediaProps_Media } from './presentView/PresentMedia'
|
||||||
|
import { sidebarPhoto_media_thumbnail } from '../sidebar/__generated__/sidebarPhoto'
|
||||||
|
|
||||||
const Gallery = styled.div`
|
const Gallery = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -31,6 +32,22 @@ const ClearWrap = styled.div`
|
||||||
clear: both;
|
clear: both;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
interface PhotoGalleryProps_Media extends PresentMediaProps_Media {
|
||||||
|
thumbnail: sidebarPhoto_media_thumbnail | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type PhotoGalleryProps = {
|
||||||
|
loading: boolean
|
||||||
|
media: PhotoGalleryProps_Media[]
|
||||||
|
activeIndex: number
|
||||||
|
presenting: boolean
|
||||||
|
onSelectImage(index: number): void
|
||||||
|
setPresenting(presenting: boolean): void
|
||||||
|
nextImage(): void
|
||||||
|
previousImage(): void
|
||||||
|
onFavorite?(): void
|
||||||
|
}
|
||||||
|
|
||||||
const PhotoGallery = ({
|
const PhotoGallery = ({
|
||||||
activeIndex = -1,
|
activeIndex = -1,
|
||||||
media,
|
media,
|
||||||
|
@ -41,42 +58,36 @@ const PhotoGallery = ({
|
||||||
nextImage,
|
nextImage,
|
||||||
previousImage,
|
previousImage,
|
||||||
onFavorite,
|
onFavorite,
|
||||||
}) => {
|
}: PhotoGalleryProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { updateSidebar } = useContext(SidebarContext)
|
const { updateSidebar } = useContext(SidebarContext)
|
||||||
|
|
||||||
const activeImage = media && activeIndex != -1 && media[activeIndex]
|
const activeImage: PhotoGalleryProps_Media | undefined = media[activeIndex]
|
||||||
|
|
||||||
const getPhotoElements = updateSidebar => {
|
let photoElements = []
|
||||||
let photoElements = []
|
if (media) {
|
||||||
if (media) {
|
photoElements = media.map((media, index) => {
|
||||||
media.filter(media => media.thumbnail)
|
const active = activeIndex == index
|
||||||
|
|
||||||
photoElements = media.map((photo, index) => {
|
return (
|
||||||
const active = activeIndex == index
|
<MediaThumbnail
|
||||||
|
key={media.id}
|
||||||
return (
|
media={media}
|
||||||
<MediaThumbnail
|
onSelectImage={index => {
|
||||||
key={photo.id}
|
updateSidebar(<MediaSidebar media={media} />)
|
||||||
media={photo}
|
onSelectImage(index)
|
||||||
onSelectImage={index => {
|
}}
|
||||||
updateSidebar(<MediaSidebar media={photo} />)
|
onFavorite={onFavorite}
|
||||||
onSelectImage(index)
|
setPresenting={setPresenting}
|
||||||
}}
|
index={index}
|
||||||
onFavorite={onFavorite}
|
active={active}
|
||||||
setPresenting={setPresenting}
|
/>
|
||||||
index={index}
|
)
|
||||||
active={active}
|
})
|
||||||
/>
|
} else {
|
||||||
)
|
for (let i = 0; i < 6; i++) {
|
||||||
})
|
photoElements.push(<PhotoThumbnail key={i} />)
|
||||||
} else {
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
photoElements.push(<PhotoThumbnail key={i} />)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return photoElements
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -85,7 +96,7 @@ const PhotoGallery = ({
|
||||||
<Loader active={loading}>
|
<Loader active={loading}>
|
||||||
{t('general.loading.media', 'Loading media')}
|
{t('general.loading.media', 'Loading media')}
|
||||||
</Loader>
|
</Loader>
|
||||||
{getPhotoElements(updateSidebar)}
|
{photoElements}
|
||||||
<PhotoFiller />
|
<PhotoFiller />
|
||||||
</Gallery>
|
</Gallery>
|
||||||
{presenting && (
|
{presenting && (
|
||||||
|
@ -98,16 +109,4 @@ const PhotoGallery = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PhotoGallery.propTypes = {
|
|
||||||
loading: PropTypes.bool,
|
|
||||||
media: PropTypes.array,
|
|
||||||
activeIndex: PropTypes.number,
|
|
||||||
presenting: PropTypes.bool,
|
|
||||||
onSelectImage: PropTypes.func,
|
|
||||||
setPresenting: PropTypes.func,
|
|
||||||
nextImage: PropTypes.func,
|
|
||||||
previousImage: PropTypes.func,
|
|
||||||
onFavorite: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PhotoGallery
|
export default PhotoGallery
|
|
@ -1,69 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
const isNativeLazyLoadSupported = 'loading' in HTMLImageElement.prototype
|
|
||||||
const placeholder = 'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
|
|
||||||
|
|
||||||
const getProtectedUrl = url => {
|
|
||||||
if (url == null) return null
|
|
||||||
|
|
||||||
const imgUrl = new URL(url, location.origin)
|
|
||||||
|
|
||||||
const tokenRegex = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/)
|
|
||||||
if (tokenRegex) {
|
|
||||||
const token = tokenRegex[1]
|
|
||||||
imgUrl.searchParams.set('token', token)
|
|
||||||
}
|
|
||||||
|
|
||||||
return imgUrl.href
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An image that needs authorization to load
|
|
||||||
* Set lazyLoading to true if you want the image to be loaded once it enters the viewport
|
|
||||||
* Native lazy load via HTMLImageElement.loading attribute will be preferred if it is supported by the browser,
|
|
||||||
* otherwise IntersectionObserver will be used.
|
|
||||||
*/
|
|
||||||
export const ProtectedImage = ({ src, lazyLoading, ...props }) => {
|
|
||||||
if (!isNativeLazyLoadSupported && lazyLoading) {
|
|
||||||
props['data-src'] = getProtectedUrl(src)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNativeLazyLoadSupported && lazyLoading) {
|
|
||||||
props.loading = 'lazy'
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
key={src}
|
|
||||||
{...props}
|
|
||||||
src={
|
|
||||||
lazyLoading && !isNativeLazyLoadSupported
|
|
||||||
? placeholder
|
|
||||||
: getProtectedUrl(src)
|
|
||||||
}
|
|
||||||
crossOrigin="use-credentials"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ProtectedImage.propTypes = {
|
|
||||||
src: PropTypes.string,
|
|
||||||
lazyLoading: PropTypes.bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProtectedVideo = ({ media, ...props }) => (
|
|
||||||
<video
|
|
||||||
{...props}
|
|
||||||
controls
|
|
||||||
key={media.id}
|
|
||||||
crossOrigin="use-credentials"
|
|
||||||
poster={getProtectedUrl(media.thumbnail?.url)}
|
|
||||||
>
|
|
||||||
<source src={getProtectedUrl(media.videoWeb.url)} type="video/mp4" />
|
|
||||||
</video>
|
|
||||||
)
|
|
||||||
|
|
||||||
ProtectedVideo.propTypes = {
|
|
||||||
media: PropTypes.object.isRequired,
|
|
||||||
}
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
import React, { DetailedHTMLProps, ImgHTMLAttributes } from 'react'
|
||||||
|
import { isNil } from '../../helpers/utils'
|
||||||
|
|
||||||
|
const isNativeLazyLoadSupported = 'loading' in HTMLImageElement.prototype
|
||||||
|
const placeholder =
|
||||||
|
'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
|
||||||
|
|
||||||
|
const getProtectedUrl = (url?: string) => {
|
||||||
|
if (url == undefined) return undefined
|
||||||
|
|
||||||
|
const imgUrl = new URL(url, location.origin)
|
||||||
|
|
||||||
|
const tokenRegex = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/)
|
||||||
|
if (tokenRegex) {
|
||||||
|
const token = tokenRegex[1]
|
||||||
|
imgUrl.searchParams.set('token', token)
|
||||||
|
}
|
||||||
|
|
||||||
|
return imgUrl.href
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProtectedImageProps
|
||||||
|
extends Omit<
|
||||||
|
DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>,
|
||||||
|
'src'
|
||||||
|
> {
|
||||||
|
src?: string
|
||||||
|
key?: string
|
||||||
|
lazyLoading?: boolean
|
||||||
|
loaded?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An image that needs authorization to load
|
||||||
|
* Set lazyLoading to true if you want the image to be loaded once it enters the viewport
|
||||||
|
* Native lazy load via HTMLImageElement.loading attribute will be preferred if it is supported by the browser,
|
||||||
|
* otherwise IntersectionObserver will be used.
|
||||||
|
*/
|
||||||
|
export const ProtectedImage = ({
|
||||||
|
src,
|
||||||
|
key,
|
||||||
|
lazyLoading,
|
||||||
|
loaded,
|
||||||
|
...props
|
||||||
|
}: ProtectedImageProps) => {
|
||||||
|
const lazyLoadProps: { 'data-src'?: string; loading?: 'lazy' | 'eager' } = {}
|
||||||
|
|
||||||
|
if (!isNativeLazyLoadSupported && lazyLoading) {
|
||||||
|
lazyLoadProps['data-src'] = getProtectedUrl(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNativeLazyLoadSupported && lazyLoading) {
|
||||||
|
lazyLoadProps.loading = 'lazy'
|
||||||
|
}
|
||||||
|
|
||||||
|
const imgSrc: string =
|
||||||
|
lazyLoading && !isNativeLazyLoadSupported
|
||||||
|
? placeholder
|
||||||
|
: getProtectedUrl(src) || placeholder
|
||||||
|
|
||||||
|
const loadedProp =
|
||||||
|
loaded !== undefined ? { loaded: loaded.toString() } : undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
key={key}
|
||||||
|
{...props}
|
||||||
|
{...lazyLoadProps}
|
||||||
|
{...loadedProp}
|
||||||
|
src={imgSrc}
|
||||||
|
crossOrigin="use-credentials"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProtectedVideoProps_Media {
|
||||||
|
__typename: 'Media'
|
||||||
|
id: string
|
||||||
|
thumbnail?: null | {
|
||||||
|
__typename: 'MediaURL'
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
videoWeb?: null | {
|
||||||
|
__typename: 'MediaURL'
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProtectedVideoProps {
|
||||||
|
media: ProtectedVideoProps_Media
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProtectedVideo = ({ media, ...props }: ProtectedVideoProps) => {
|
||||||
|
if (isNil(media.videoWeb)) {
|
||||||
|
console.error('ProetctedVideo called with media.videoWeb = null')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
{...props}
|
||||||
|
controls
|
||||||
|
key={media.id}
|
||||||
|
crossOrigin="use-credentials"
|
||||||
|
poster={getProtectedUrl(media.thumbnail?.url)}
|
||||||
|
>
|
||||||
|
<source src={getProtectedUrl(media.videoWeb.url)} type="video/mp4" />
|
||||||
|
</video>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL mutation operation: markMediaFavorite
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface markMediaFavorite_favoriteMedia {
|
||||||
|
__typename: 'Media'
|
||||||
|
id: string
|
||||||
|
favorite: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface markMediaFavorite {
|
||||||
|
/**
|
||||||
|
* Mark or unmark a media as being a favorite
|
||||||
|
*/
|
||||||
|
favoriteMedia: markMediaFavorite_favoriteMedia
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface markMediaFavoriteVariables {
|
||||||
|
mediaId: string
|
||||||
|
favorite: boolean
|
||||||
|
}
|
|
@ -1,53 +0,0 @@
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import React from 'react'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
import { ProtectedImage, ProtectedVideo } from '../ProtectedMedia'
|
|
||||||
|
|
||||||
const StyledPhoto = styled(ProtectedImage)`
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
object-fit: contain;
|
|
||||||
object-position: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
const StyledVideo = styled(ProtectedVideo)`
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
`
|
|
||||||
|
|
||||||
const PresentMedia = ({ media, imageLoaded, ...otherProps }) => {
|
|
||||||
if (media.type == 'photo') {
|
|
||||||
return (
|
|
||||||
<div {...otherProps}>
|
|
||||||
<StyledPhoto src={media.thumbnail?.url} />
|
|
||||||
<StyledPhoto
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
src={media.highRes?.url}
|
|
||||||
onLoad={e => {
|
|
||||||
e.target.style.display = 'initial'
|
|
||||||
imageLoaded && imageLoaded()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media.type == 'video') {
|
|
||||||
return <StyledVideo media={media} />
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unknown media type '${media.type}'`)
|
|
||||||
}
|
|
||||||
|
|
||||||
PresentMedia.propTypes = {
|
|
||||||
media: PropTypes.object.isRequired,
|
|
||||||
imageLoaded: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PresentMedia
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue