Merge github.com:photoview/photoview
This commit is contained in:
commit
15639a9478
|
@ -60,7 +60,7 @@ jobs:
|
|||
OUTPUT_PLATFORM=$(echo ${{ matrix.target_platform }} | sed 's/\//-/g')
|
||||
echo ::set-output name=output_platform::${OUTPUT_PLATFORM}
|
||||
|
||||
TAG="--tag ${DOCKER_IMAGE}:${OUTPUT_PLATFORM}-${GITHUB_SHA::8}"
|
||||
TAG="--tag ${DOCKER_IMAGE}:${OUTPUT_PLATFORM}-${GITHUB_SHA::8}-${{ github.event_name }}"
|
||||
|
||||
echo ::set-output name=docker_username::${DOCKER_USERNAME}
|
||||
echo ::set-output name=docker_image::${DOCKER_IMAGE}
|
||||
|
@ -97,6 +97,9 @@ jobs:
|
|||
name: Combine Docker Images
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.event_name != 'pull_request' && github.repository == 'photoview/photoview'
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
needs: [build]
|
||||
|
||||
|
@ -107,10 +110,10 @@ jobs:
|
|||
|
||||
- name: Create Manifests
|
||||
run: |
|
||||
DOCKER_IMAGES="${DOCKER_IMAGE}:linux-amd64-${GITHUB_SHA::8}"
|
||||
DOCKER_IMAGES="${DOCKER_IMAGES} ${DOCKER_IMAGE}:linux-arm64-${GITHUB_SHA::8}"
|
||||
DOCKER_IMAGES="${DOCKER_IMAGES} ${DOCKER_IMAGE}:linux-arm-v7-${GITHUB_SHA::8}"
|
||||
DOCKER_IMAGES="${DOCKER_IMAGES} ${DOCKER_IMAGE}:linux-arm-v6-${GITHUB_SHA::8}"
|
||||
DOCKER_IMAGES="${DOCKER_IMAGE}:linux-amd64-${GITHUB_SHA::8}-${{ github.event_name }}"
|
||||
DOCKER_IMAGES="${DOCKER_IMAGES} ${DOCKER_IMAGE}:linux-arm64-${GITHUB_SHA::8}-${{ github.event_name }}"
|
||||
DOCKER_IMAGES="${DOCKER_IMAGES} ${DOCKER_IMAGE}:linux-arm-v7-${GITHUB_SHA::8}-${{ github.event_name }}"
|
||||
DOCKER_IMAGES="${DOCKER_IMAGES} ${DOCKER_IMAGE}:linux-arm-v6-${GITHUB_SHA::8}-${{ github.event_name }}"
|
||||
|
||||
VERSION=edge
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
|
@ -122,7 +125,7 @@ jobs:
|
|||
if [[ $VERSION =~ ^(([0-9]{1,3})\.[0-9]{1,3})\.[0-9]{1,3}$ ]]; then
|
||||
VERSION_MINOR=${BASH_REMATCH[1]}
|
||||
VERSION_MAJOR=${BASH_REMATCH[2]}
|
||||
TAGS+=("${VERSION_MAJOR}", "${VERSION_MINOR}")
|
||||
TAGS+=("${VERSION_MAJOR}" "${VERSION_MINOR}" "latest")
|
||||
fi
|
||||
|
||||
for TAG in ${TAGS[*]}; do
|
||||
|
@ -141,7 +144,7 @@ jobs:
|
|||
|
||||
PLATFORMS=("amd64" "arm64" "arm-v7" "arm-v6")
|
||||
for PLATFORM in ${PLATFORMS[@]}; do
|
||||
TAG="linux-${PLATFORM}-${GITHUB_SHA::8}"
|
||||
TAG="linux-${PLATFORM}-${GITHUB_SHA::8}-${{ github.event_name }}"
|
||||
echo "Deleting tag: ${DOCKER_IMAGE}:${TAG}"
|
||||
|
||||
curl -X DELETE \
|
||||
|
@ -149,3 +152,8 @@ jobs:
|
|||
-H "Authorization: JWT ${ACCESS_TOKEN}" \
|
||||
https://hub.docker.com/v2/repositories/${DOCKER_IMAGE}/tags/${TAG}/
|
||||
done
|
||||
|
||||
- name: Clear
|
||||
if: always()
|
||||
run: |
|
||||
rm -f ${HOME}/.docker/config.json
|
||||
|
|
|
@ -56,7 +56,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 14.x]
|
||||
node-version: [15.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
|
@ -21,6 +21,7 @@ node_modules/
|
|||
.cache/
|
||||
dist/
|
||||
build/
|
||||
*.tsbuildinfo
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
### Build UI ###
|
||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} node:10 as ui
|
||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} node:15 as ui
|
||||
|
||||
ARG PHOTOVIEW_API_ENDPOINT
|
||||
ENV PHOTOVIEW_API_ENDPOINT=${PHOTOVIEW_API_ENDPOINT}
|
||||
|
|
19
README.md
19
README.md
|
@ -143,7 +143,18 @@ And the graphql playground at [localhost:4001](http://localhost:4001)
|
|||
|
||||
## Sponsors
|
||||
|
||||
<a href="https://github.com/ericerkz">
|
||||
<img src="https://avatars.githubusercontent.com/u/79728329?v=4" height="auto" width="100" style="border-radius:50%"><br/>
|
||||
<b>@ericerkz</b>
|
||||
</a>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://github.com/ericerkz">
|
||||
<img src="https://avatars.githubusercontent.com/u/79728329?v=4" height="auto" width="100" style="border-radius:50%"><br/>
|
||||
<b>@ericerkz</b>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://github.com/robin-moser">
|
||||
<img src="https://avatars.githubusercontent.com/u/26254821?v=4" height="auto" width="100" style="border-radius:50%"><br/>
|
||||
<b>@robin-moser</b>
|
||||
</a>
|
||||
</td>
|
||||
</table>
|
||||
|
|
|
@ -152,7 +152,7 @@ func SetupDatabase() (*gorm.DB, error) {
|
|||
}
|
||||
|
||||
func MigrateDatabase(db *gorm.DB) error {
|
||||
db.AutoMigrate(
|
||||
err := db.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.AccessToken{},
|
||||
&models.SiteInfo{},
|
||||
|
@ -163,16 +163,28 @@ func MigrateDatabase(db *gorm.DB) error {
|
|||
&models.VideoMetadata{},
|
||||
&models.ShareToken{},
|
||||
&models.UserMediaData{},
|
||||
&models.UserPreferences{},
|
||||
|
||||
// Face detection
|
||||
&models.FaceGroup{},
|
||||
&models.ImageFace{},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Auto migration failed: %v\n", err)
|
||||
}
|
||||
|
||||
// v2.1.0 - Replaced by Media.CreatedAt
|
||||
if db.Migrator().HasColumn(&models.Media{}, "date_imported") {
|
||||
db.Migrator().DropColumn(&models.Media{}, "date_imported")
|
||||
}
|
||||
|
||||
// v2.3.0 - Changed type of MediaEXIF.Exposure and MediaEXIF.Flash
|
||||
// from string values to decimal and int respectively
|
||||
err = migrate_exif_fields(db)
|
||||
if err != nil {
|
||||
log.Printf("Failed to run exif fields migration: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Migrate MediaExif fields "exposure" and "flash" from strings to integers
|
||||
func migrate_exif_fields(db *gorm.DB) error {
|
||||
mediaExifColumns, err := db.Migrator().ColumnTypes(&models.MediaEXIF{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.Transaction(func(tx *gorm.DB) error {
|
||||
for _, exifCol := range mediaExifColumns {
|
||||
if exifCol.Name() == "exposure" {
|
||||
switch exifCol.DatabaseTypeName() {
|
||||
case "double", "numeric", "real":
|
||||
// correct type, do nothing
|
||||
default:
|
||||
// do migration
|
||||
if err := migrate_exif_fields_exposure(db); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if exifCol.Name() == "flash" {
|
||||
switch exifCol.DatabaseTypeName() {
|
||||
case "double", "numeric", "real":
|
||||
// correct type, do nothing
|
||||
default:
|
||||
// do migration
|
||||
if err := migrate_exif_fields_flash(db); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(&models.MediaEXIF{}); err != nil {
|
||||
return errors.Wrap(err, "failed to auto migrate media_exif after exposure conversion")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrate_exif_fields_exposure(db *gorm.DB) error {
|
||||
log.Println("Migrating `media_exif.exposure` from string to double")
|
||||
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
if err := tx.Exec("UPDATE media_exif SET exposure = NULL WHERE exposure = ''").Error; err != nil {
|
||||
return errors.Wrapf(err, "convert flash attribute empty values to NULL")
|
||||
}
|
||||
|
||||
type exifModel struct {
|
||||
ID int `gorm:"primarykey"`
|
||||
Exposure *string
|
||||
}
|
||||
var results []exifModel
|
||||
|
||||
return tx.Model(&exifModel{}).Table("media_exif").Where("exposure LIKE '%/%'").FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
|
||||
for _, result := range results {
|
||||
|
||||
if result.Exposure == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
frac := strings.Split(*result.Exposure, "/")
|
||||
if len(frac) != 2 {
|
||||
return errors.Errorf("failed to convert exposure value (%s) expected format x/y", frac)
|
||||
}
|
||||
|
||||
numerator, err := strconv.ParseFloat(frac[0], 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
denominator, err := strconv.ParseFloat(frac[1], 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
decimalValue := numerator / denominator
|
||||
*result.Exposure = fmt.Sprintf("%f", decimalValue)
|
||||
}
|
||||
|
||||
tx.Save(&results)
|
||||
|
||||
return nil
|
||||
}).Error
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "migrating `media_exif.exposure` failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrate_exif_fields_flash(db *gorm.DB) error {
|
||||
log.Println("Migrating `media_exif.flash` from string to int")
|
||||
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
if err := tx.Exec("UPDATE media_exif SET flash = NULL WHERE flash = ''").Error; err != nil {
|
||||
return errors.Wrapf(err, "convert flash attribute empty values to NULL")
|
||||
}
|
||||
|
||||
type exifModel struct {
|
||||
ID int `gorm:"primarykey"`
|
||||
Flash *string
|
||||
}
|
||||
var results []exifModel
|
||||
|
||||
var flashDescriptions = map[int]string{
|
||||
0x0: "No Flash",
|
||||
0x1: "Fired",
|
||||
0x5: "Fired, Return not detected",
|
||||
0x7: "Fired, Return detected",
|
||||
0x8: "On, Did not fire",
|
||||
0x9: "On, Fired",
|
||||
0xD: "On, Return not detected",
|
||||
0xF: "On, Return detected",
|
||||
0x10: "Off, Did not fire",
|
||||
0x14: "Off, Did not fire, Return not detected",
|
||||
0x18: "Auto, Did not fire",
|
||||
0x19: "Auto, Fired",
|
||||
0x1D: "Auto, Fired, Return not detected",
|
||||
0x1F: "Auto, Fired, Return detected",
|
||||
0x20: "No flash function",
|
||||
0x30: "Off, No flash function",
|
||||
0x41: "Fired, Red-eye reduction",
|
||||
0x45: "Fired, Red-eye reduction, Return not detected",
|
||||
0x47: "Fired, Red-eye reduction, Return detected",
|
||||
0x49: "On, Red-eye reduction",
|
||||
0x4D: "On, Red-eye reduction, Return not detected",
|
||||
0x4F: "On, Red-eye reduction, Return detected",
|
||||
0x50: "Off, Red-eye reduction",
|
||||
0x58: "Auto, Did not fire, Red-eye reduction",
|
||||
0x59: "Auto, Fired, Red-eye reduction",
|
||||
0x5D: "Auto, Fired, Red-eye reduction, Return not detected",
|
||||
0x5F: "Auto, Fired, Red-eye reduction, Return detected",
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
if result.Flash == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for index, name := range flashDescriptions {
|
||||
if *result.Flash == name {
|
||||
*result.Flash = fmt.Sprintf("%d", index)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tx.Save(&results)
|
||||
|
||||
return nil
|
||||
}).Error
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "migrating `media_exif.flash` failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
225
api/go.sum
225
api/go.sum
|
@ -1,115 +1,185 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/99designs/gqlgen v0.13.0 h1:haLTcUp3Vwp80xMVEg5KRNwzfUrgFdRmtBY8fuB8scA=
|
||||
github.com/99designs/gqlgen v0.13.0/go.mod h1:NV130r6f4tpRWuAI+zsrSdooO/eWUv+Gyyoi3rEfXIk=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Kagami/go-face v0.0.0-20200825065730-3dd2d74dccfb h1:DXwA1Te9paM+nsdTGc7uve37lq7WEbQO+gwGBPVwQuQ=
|
||||
github.com/Kagami/go-face v0.0.0-20200825065730-3dd2d74dccfb/go.mod h1:9wdDJkRgo3SGTcFwbQ7elVIQhIr2bbBjecuY7VoqmPU=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Shopify/sarama v1.19.0 h1:9oksLxC6uxVPHPVYUmq6xhr1BOF/hHobWH2UzO67z1s=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
|
||||
github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM=
|
||||
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI=
|
||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw=
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/aws/aws-lambda-go v1.13.3 h1:SuCy7H3NLyp+1Mrfp+m80jcbi9KYWAs9/BXwppwRDzY=
|
||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.27.0 h1:0xphMHGMLBrPMfxR2AmVjZKcMEESEgWF8Kru94BNByk=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0 h1:qZ+woO4SamnH/eEbjM2IDLhRNwIwND/RQyVlBLp3Jqg=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
github.com/barasher/go-exiftool v1.4.0 h1:fTQsz1lWS15rRl13aFRjENt0gUXC+ZVBo/vICacn98A=
|
||||
github.com/barasher/go-exiftool v1.4.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/casbin/casbin/v2 v2.1.2 h1:bTwon/ECRx9dwBy2ewRVr5OiqjeXSGiTUY74sDPQi/g=
|
||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec h1:EdRZT3IeKQmfCSrgo8SZ8V3MEnskuJP0wCYNpe+aiXo=
|
||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
||||
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w=
|
||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
||||
github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf h1:CAKfRE2YtTUIjjh1bkBtyYFaUT/WmOqsJjgtihT0vMI=
|
||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw=
|
||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473 h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db h1:gb2Z18BhTPJPpLQWj4T+rfKHYCHxRHCtRxhKKjRidVw=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ=
|
||||
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo=
|
||||
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/googleapis v1.1.0 h1:kFkMAZBNAn4j7K0GiZr8cRYzejq68VbheufiV3YuyFI=
|
||||
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
|
||||
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
|
@ -121,36 +191,60 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
|
|||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/h2non/filetype v1.1.1 h1:xvOwnXKAckvtLWsN398qS9QhlxlnVXBjXBydK2/UFB4=
|
||||
github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
||||
github.com/hashicorp/consul/api v1.3.0 h1:HXNYlRkkM/t+Y/Yhxtwcy02dlYwIaoxzvxPnS+cqy78=
|
||||
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
|
||||
github.com/hashicorp/consul/sdk v0.3.0 h1:UOxjlb4xVNF93jak1mzzoBatyFju9nrkxpVwIp/QqxQ=
|
||||
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0 h1:WhIgCr5a7AaVH6jPUwjtRuuE7/RDufnUvzIr48smyxs=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/hudl/fargo v1.3.0 h1:0U6+BtN6LhaYuTnIJq4Wyq5cpn6O2kWrxAtcqBmYY6w=
|
||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d h1:/WZQPMZNsjZ7IlCpsLGdQBINg5bxKQ1K1sh6awxLtkA=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
|
@ -168,6 +262,7 @@ github.com/jackc/pgconn v1.8.1 h1:ySBX7Q87vOMqKU2bbmKbUvtYhauDFclYbNDYIE1/h6s=
|
|||
github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
|
@ -206,43 +301,63 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f
|
|||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3 h1:JnPg/5Q9xVJGfjsO5CPUOjnJps1JaRUm8I9FXVCFK94=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
|
||||
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0 h1:ZqfnKyx9KGpRcW04j5nnPDgRgoXUeLh2YFBeFzphcA0=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/lyft/protoc-gen-validate v0.0.13 h1:KNt/RhmQTOLr7Aj8PsJ7mTronaFyx80mRTT9qF261dA=
|
||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
|
||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
|
@ -250,17 +365,26 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
|||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
|
@ -268,132 +392,203 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
|
|||
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223 h1:F9x/1yl3T2AeKLr2AMdilSD8+f9bvMnNN8VS5iDtovc=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/jwt v0.3.2 h1:+RB5hMpXUUA2dfxuhBTEkMOrYmM+gKIZYS1KjSostMI=
|
||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
||||
github.com/nats-io/nats-server/v2 v2.1.2 h1:i2Ly0B+1+rzNZHHWtD4ZwKi+OU5l+uQo1iDHZ2PmiIc=
|
||||
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
|
||||
github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ=
|
||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nkeys v0.1.3 h1:6JrEfig+HzTH85yxzhSVbjHRJv9cn0p6n3IngIcM5/k=
|
||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk=
|
||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5 h1:58+kh9C6jJVXYjt8IE48G2eWl6BjwU5Gj0gqY84fy78=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU=
|
||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
||||
github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 h1:ZCnq+JUrvXcDVhX/xRolRBZifmabN1HcS1wrPSvxhrU=
|
||||
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/openzipkin/zipkin-go v0.2.2 h1:nY8Hti+WKaP0cRsSeQ026wU03QsM762XBeCXBb9NAWI=
|
||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q=
|
||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/performancecopilot/speed v3.0.0+incompatible h1:2WnRzIquHa5QxaJKShDkLM+sc0JPuwhXzK8OYOyt3Vg=
|
||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE=
|
||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.3.0 h1:miYCvYqFXtl/J9FIy8eNpBfYthAEFg+Ys0XyUVEcDsc=
|
||||
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.1.0 h1:ElTg5tNp4DqfV7UQjDqv2+RJlNzsDtvNAWccbItceIE=
|
||||
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
|
||||
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
|
||||
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f h1:UFr9zpz4xgTnIE5yIMtWAMngCdZ9p/+q6lTbgelo80M=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f h1:8P2MkG70G76gnZBOPGwmMIgwBb/rESQuwsJ7K8ds4NE=
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0 h1:JJV9CsgM9EC9w2iVkwuz+sMx8yRFe89PJRUrv6hPCIA=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ=
|
||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271 h1:WhxRHzgeVGETMlmVfqhRn8RIeeNoPr2Czh33I4Zdccw=
|
||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a h1:AhmOdSHeswKHBjhsLs/7+1voOxT+LLrSk/Nxvk35fug=
|
||||
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/strukturag/libheif v1.11.0 h1:HaWu5re98INSXNq7C8o5AwLcv2qD8+U7a+jVCpGWemI=
|
||||
github.com/strukturag/libheif v1.11.0/go.mod h1:E/PNRlmVtrtj9j2AvBZlrO4dsBDu6KfwDZn7X1Ce8Ks=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
|
||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
||||
github.com/vektah/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns=
|
||||
github.com/vektah/gqlparser/v2 v2.1.0/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
|
||||
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlVjOeLOBz+CPAI8dnbqNSVwUwRrkp7vQ=
|
||||
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xor-gate/goexif2 v1.1.0 h1:OvTZ5iEvsDhRWFjV5xY3wT7uHFna28nSSP7ucau+cXQ=
|
||||
github.com/xor-gate/goexif2 v1.1.0/go.mod h1:eRjn3VSkAwpNpxEx/CGmd0zg0JFGL3akrSMxnJ581AY=
|
||||
github.com/zenazn/goji v0.9.0 h1:RSQQAbXGArQ0dIDEq+PI6WqN6if+5KHu6x2Cx/GXLTQ=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 h1:VcrIfasaLFkyjk6KNlXQSzO+B0fZcnECiDrKJsfxka0=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
|
@ -408,6 +603,7 @@ golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPh
|
|||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
|
||||
|
@ -416,8 +612,10 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
|
|||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -435,14 +633,17 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
|
|||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -464,7 +665,9 @@ golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
|
@ -472,6 +675,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
@ -490,6 +694,7 @@ golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM=
|
||||
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -498,14 +703,17 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
|||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.3.1 h1:oJra/lMfmtm13/rgY/8i3MzjFWYXvQIAKjQ3HqofMk8=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
|
@ -515,25 +723,38 @@ google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij
|
|||
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec h1:RlWgLqCMMIYYEVcAR5MDsuHlVkaIPDAF+5Dehzg8L5A=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/vansante/go-ffprobe.v2 v2.0.2 h1:DdxSfFnlqeawPIVbIQEI6LR6OQHQNR7tNgWb2mWuC4w=
|
||||
gopkg.in/vansante/go-ffprobe.v2 v2.0.2/go.mod h1:qF0AlAjk7Nqzqf3y333Ly+KxN3cKF2JqA3JT5ZheUGE=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.0.5 h1:WAAmvLK2rG0tCOqrf5XcLi2QUwugd4rcVJ/W3aoon9o=
|
||||
gorm.io/driver/mysql v1.0.5/go.mod h1:N1OIhHAIhx5SunkMGqWbGFVeh4yTNWKmMo1GOAsohLI=
|
||||
|
@ -549,8 +770,12 @@ gorm.io/gorm v1.21.6/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
|||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 h1:ucqkfpjg9WzSUubAO62csmucvxl4/JeW3F4I4909XkM=
|
||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67 h1:e1sMhtVq9AfcEy8AXNb8eSg6gbzfdpYhoNqnPJa+GzI=
|
||||
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
|
||||
|
|
|
@ -27,6 +27,8 @@ models:
|
|||
fields:
|
||||
albums:
|
||||
resolver: true
|
||||
UserPreferences:
|
||||
model: github.com/photoview/photoview/api/graphql/models.UserPreferences
|
||||
Media:
|
||||
model: github.com/photoview/photoview/api/graphql/models.Media
|
||||
fields:
|
||||
|
|
|
@ -6,17 +6,22 @@ import (
|
|||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/photoview/photoview/api/graphql/auth"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func IsAdmin(database *gorm.DB) func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
|
||||
return func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
|
||||
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil || user.Admin == false {
|
||||
return nil, errors.New("user must be admin")
|
||||
}
|
||||
|
||||
return next(ctx)
|
||||
func IsAdmin(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil || user.Admin == false {
|
||||
return nil, errors.New("user must be admin")
|
||||
}
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
func IsAuthorized(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, auth.ErrUnauthorized
|
||||
}
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -68,11 +68,52 @@ type TimelineGroup struct {
|
|||
Date time.Time `json:"date"`
|
||||
}
|
||||
|
||||
type LanguageTranslation string
|
||||
|
||||
const (
|
||||
LanguageTranslationEnglish LanguageTranslation = "English"
|
||||
LanguageTranslationDanish LanguageTranslation = "Danish"
|
||||
)
|
||||
|
||||
var AllLanguageTranslation = []LanguageTranslation{
|
||||
LanguageTranslationEnglish,
|
||||
LanguageTranslationDanish,
|
||||
}
|
||||
|
||||
func (e LanguageTranslation) IsValid() bool {
|
||||
switch e {
|
||||
case LanguageTranslationEnglish, LanguageTranslationDanish:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e LanguageTranslation) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *LanguageTranslation) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = LanguageTranslation(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid LanguageTranslation", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e LanguageTranslation) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
type MediaType string
|
||||
|
||||
const (
|
||||
MediaTypePhoto MediaType = "photo"
|
||||
MediaTypeVideo MediaType = "video"
|
||||
MediaTypePhoto MediaType = "Photo"
|
||||
MediaTypeVideo MediaType = "Video"
|
||||
)
|
||||
|
||||
var AllMediaType = []MediaType{
|
||||
|
|
|
@ -39,12 +39,30 @@ func (Media) TableName() string {
|
|||
}
|
||||
|
||||
func (m *Media) BeforeSave(tx *gorm.DB) error {
|
||||
// Update hashes
|
||||
// Update path hash
|
||||
m.PathHash = MD5Hash(m.Path)
|
||||
|
||||
if m.SideCarPath != nil {
|
||||
encodedHash := MD5Hash(*m.SideCarPath)
|
||||
m.SideCarHash = &encodedHash
|
||||
// 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
|
||||
|
|
|
@ -36,6 +36,13 @@ type AccessToken struct {
|
|||
Expire time.Time `gorm:"not null;index"`
|
||||
}
|
||||
|
||||
type UserPreferences struct {
|
||||
Model
|
||||
UserID int `gorm:"not null;index"`
|
||||
User User `gorm:"constraint:OnDelete:CASCADE;"`
|
||||
Language *LanguageTranslation
|
||||
}
|
||||
|
||||
var ErrorInvalidUserCredentials = errors.New("invalid credentials")
|
||||
|
||||
func AuthorizeUser(db *gorm.DB, username string, password string) (*User, error) {
|
||||
|
|
|
@ -154,6 +154,49 @@ func (r *mutationResolver) InitialSetupWizard(ctx context.Context, username stri
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) MyUserPreferences(ctx context.Context) (*models.UserPreferences, error) {
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, auth.ErrUnauthorized
|
||||
}
|
||||
|
||||
userPref := models.UserPreferences{
|
||||
UserID: user.ID,
|
||||
}
|
||||
if err := r.Database.Where("user_id = ?", user.ID).FirstOrCreate(&userPref).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &userPref, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ChangeUserPreferences(ctx context.Context, language *string) (*models.UserPreferences, error) {
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, auth.ErrUnauthorized
|
||||
}
|
||||
|
||||
var langTrans *models.LanguageTranslation = nil
|
||||
if language != nil {
|
||||
lng := models.LanguageTranslation(*language)
|
||||
langTrans = &lng
|
||||
}
|
||||
|
||||
var userPref models.UserPreferences
|
||||
if err := r.Database.Where("user_id = ?", user.ID).FirstOrInit(&userPref).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userPref.UserID = user.ID
|
||||
userPref.Language = langTrans
|
||||
|
||||
if err := r.Database.Save(&userPref).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &userPref, nil
|
||||
}
|
||||
|
||||
// Admin queries
|
||||
func (r *mutationResolver) UpdateUser(ctx context.Context, id int, username *string, password *string, admin *bool) (*models.User, error) {
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
directive @isAuthorized on FIELD_DEFINITION
|
||||
directive @isAdmin on FIELD_DEFINITION
|
||||
|
||||
scalar Time
|
||||
|
@ -30,7 +31,9 @@ type Query {
|
|||
"List of registered users, must be admin to call"
|
||||
user(order: Ordering, paginate: Pagination): [User!]! @isAdmin
|
||||
"Information about the currently logged in user"
|
||||
myUser: User!
|
||||
myUser: User! @isAuthorized
|
||||
|
||||
myUserPreferences: UserPreferences! @isAuthorized
|
||||
|
||||
"List of albums owned by the logged in user."
|
||||
myAlbums(
|
||||
|
@ -42,7 +45,7 @@ type Query {
|
|||
showEmpty: Boolean
|
||||
"Show only albums having favorites"
|
||||
onlyWithFavorites: Boolean
|
||||
): [Album!]!
|
||||
): [Album!]! @isAuthorized
|
||||
"""
|
||||
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
|
||||
|
@ -50,7 +53,7 @@ type Query {
|
|||
album(id: ID!, tokenCredentials: ShareTokenCredentials): Album!
|
||||
|
||||
"List of media owned by the logged in user"
|
||||
myMedia(order: Ordering, paginate: Pagination): [Media!]!
|
||||
myMedia(order: Ordering, paginate: Pagination): [Media!]! @isAuthorized
|
||||
"""
|
||||
Get media by id, user must own the media or be admin.
|
||||
If valid tokenCredentials are provided, the media may be retrived without further authentication
|
||||
|
@ -60,10 +63,10 @@ type Query {
|
|||
"Get a list of media by their ids, user must own the media or be admin"
|
||||
mediaList(ids: [ID!]!): [Media!]!
|
||||
|
||||
myTimeline(paginate: Pagination, onlyFavorites: Boolean): [TimelineGroup!]!
|
||||
myTimeline(paginate: Pagination, onlyFavorites: Boolean): [TimelineGroup!]! @isAuthorized
|
||||
|
||||
"Get media owned by the logged in user, returned in GeoJson format"
|
||||
myMediaGeoJson: Any!
|
||||
myMediaGeoJson: Any! @isAuthorized
|
||||
"Get the mapbox api token, returns null if mapbox is not enabled"
|
||||
mapboxToken: String
|
||||
|
||||
|
@ -72,8 +75,8 @@ type Query {
|
|||
|
||||
search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult!
|
||||
|
||||
myFaceGroups(paginate: Pagination): [FaceGroup!]!
|
||||
faceGroup(id: ID!): FaceGroup!
|
||||
myFaceGroups(paginate: Pagination): [FaceGroup!]! @isAuthorized
|
||||
faceGroup(id: ID!): FaceGroup! @isAuthorized
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
|
@ -89,32 +92,32 @@ type Mutation {
|
|||
"Scan all users for new media"
|
||||
scanAll: ScannerResult! @isAdmin
|
||||
"Scan a single user for new media"
|
||||
scanUser(userId: ID!): ScannerResult!
|
||||
scanUser(userId: ID!): ScannerResult! @isAdmin
|
||||
|
||||
"Generate share token for album"
|
||||
shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken
|
||||
shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken! @isAuthorized
|
||||
"Generate share token for media"
|
||||
shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken
|
||||
shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken! @isAuthorized
|
||||
"Delete a share token by it's token value"
|
||||
deleteShareToken(token: String!): ShareToken
|
||||
deleteShareToken(token: String!): ShareToken! @isAuthorized
|
||||
"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
|
||||
protectShareToken(token: String!, password: String): ShareToken! @isAuthorized
|
||||
|
||||
"Mark or unmark a media as being a favorite"
|
||||
favoriteMedia(mediaId: ID!, favorite: Boolean!): Media
|
||||
favoriteMedia(mediaId: ID!, favorite: Boolean!): Media! @isAuthorized
|
||||
|
||||
updateUser(
|
||||
id: ID!
|
||||
username: String
|
||||
password: String
|
||||
admin: Boolean
|
||||
): User @isAdmin
|
||||
): User! @isAdmin
|
||||
createUser(
|
||||
username: String!
|
||||
password: String
|
||||
admin: Boolean!
|
||||
): User @isAdmin
|
||||
deleteUser(id: ID!): User @isAdmin
|
||||
): User! @isAdmin
|
||||
deleteUser(id: ID!): User! @isAdmin
|
||||
|
||||
"Add a root path from where to look for media for the given user"
|
||||
userAddRootPath(id: ID!, rootPath: String!): Album @isAdmin
|
||||
|
@ -124,21 +127,23 @@ type Mutation {
|
|||
Set how often, in seconds, the server should automatically scan for new media,
|
||||
a value of 0 will disable periodic scans
|
||||
"""
|
||||
setPeriodicScanInterval(interval: Int!): Int!
|
||||
setPeriodicScanInterval(interval: Int!): Int! @isAdmin
|
||||
|
||||
"Set max number of concurrent scanner jobs running at once"
|
||||
setScannerConcurrentWorkers(workers: Int!): Int!
|
||||
setScannerConcurrentWorkers(workers: Int!): Int! @isAdmin
|
||||
|
||||
changeUserPreferences(language: String): UserPreferences! @isAuthorized
|
||||
|
||||
"Assign a label to a face group, set label to null to remove the current one"
|
||||
setFaceGroupLabel(faceGroupID: ID!, label: String): FaceGroup!
|
||||
setFaceGroupLabel(faceGroupID: ID!, label: String): FaceGroup! @isAuthorized
|
||||
"Merge two face groups into a single one, all ImageFaces from source will be moved to destination"
|
||||
combineFaceGroups(destinationFaceGroupID: ID!, sourceFaceGroupID: ID!): FaceGroup!
|
||||
combineFaceGroups(destinationFaceGroupID: ID!, sourceFaceGroupID: ID!): FaceGroup! @isAuthorized
|
||||
"Move a list of ImageFaces to another face group"
|
||||
moveImageFaces(imageFaceIDs: [ID!]!, destinationFaceGroupID: ID!): FaceGroup!
|
||||
moveImageFaces(imageFaceIDs: [ID!]!, destinationFaceGroupID: ID!): FaceGroup! @isAuthorized
|
||||
"Check all unlabeled faces to see if they match a labeled FaceGroup, and move them if they match"
|
||||
recognizeUnlabeledFaces: [ImageFace!]!
|
||||
recognizeUnlabeledFaces: [ImageFace!]! @isAuthorized
|
||||
"Move a list of ImageFaces to a new face group"
|
||||
detachImageFaces(imageFaceIDs: [ID!]!): FaceGroup!
|
||||
detachImageFaces(imageFaceIDs: [ID!]!): FaceGroup! @isAuthorized
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
|
@ -216,6 +221,16 @@ type User {
|
|||
#shareTokens: [ShareToken]
|
||||
}
|
||||
|
||||
enum LanguageTranslation {
|
||||
English,
|
||||
Danish
|
||||
}
|
||||
|
||||
type UserPreferences {
|
||||
id: ID!
|
||||
language: LanguageTranslation
|
||||
}
|
||||
|
||||
type Album {
|
||||
id: ID!
|
||||
title: String!
|
||||
|
@ -264,8 +279,8 @@ type MediaDownload {
|
|||
}
|
||||
|
||||
enum MediaType {
|
||||
photo
|
||||
video
|
||||
Photo
|
||||
Video
|
||||
}
|
||||
|
||||
type Media {
|
||||
|
|
|
@ -14,6 +14,22 @@ type exifParser interface {
|
|||
ParseExif(media *models.Media) (*models.MediaEXIF, error)
|
||||
}
|
||||
|
||||
var use_exiftool bool = false
|
||||
|
||||
func InitializeEXIFParser() {
|
||||
// Decide between internal or external Exif parser
|
||||
et, err := exiftool.NewExiftool()
|
||||
|
||||
if err != nil {
|
||||
use_exiftool = false
|
||||
log.Printf("Failed to get exiftool, using internal exif parser instead: %v\n", err)
|
||||
} else {
|
||||
et.Close()
|
||||
log.Println("Found exiftool")
|
||||
use_exiftool = true
|
||||
}
|
||||
}
|
||||
|
||||
// SaveEXIF scans the media file for exif metadata and saves it in the database if found
|
||||
func SaveEXIF(tx *gorm.DB, media *models.Media) (*models.MediaEXIF, error) {
|
||||
|
||||
|
@ -32,14 +48,11 @@ func SaveEXIF(tx *gorm.DB, media *models.Media) (*models.MediaEXIF, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Decide between internal or external Exif parser
|
||||
et, err := exiftool.NewExiftool()
|
||||
et.Close()
|
||||
var parser exifParser
|
||||
if err != nil {
|
||||
parser = &internalExifParser{}
|
||||
} else {
|
||||
if use_exiftool {
|
||||
parser = &externalExifParser{}
|
||||
} else {
|
||||
parser = &internalExifParser{}
|
||||
}
|
||||
|
||||
exif, err := parser.ParseExif(media)
|
||||
|
|
|
@ -2,8 +2,6 @@ package exif
|
|||
|
||||
import (
|
||||
"log"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/barasher/go-exiftool"
|
||||
|
@ -26,35 +24,31 @@ func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models.
|
|||
|
||||
for _, fileInfo := range fileInfos {
|
||||
if fileInfo.Err != nil {
|
||||
log.Printf("Fileinfo error\n")
|
||||
log.Printf("Fileinfo error: %v\n", fileInfo.Err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get camera model
|
||||
model, err := fileInfo.GetString("Model")
|
||||
if err == nil {
|
||||
log.Printf("Camera model: %v", model)
|
||||
newExif.Camera = &model
|
||||
}
|
||||
|
||||
// Get Camera make
|
||||
make, err := fileInfo.GetString("Make")
|
||||
if err == nil {
|
||||
log.Printf("Camera make: %v", make)
|
||||
newExif.Maker = &make
|
||||
}
|
||||
|
||||
// Get lens
|
||||
lens, err := fileInfo.GetString("LensModel")
|
||||
if err == nil {
|
||||
log.Printf("Lens: %v", lens)
|
||||
newExif.Lens = &lens
|
||||
}
|
||||
|
||||
//Get time of photo
|
||||
date, err := fileInfo.GetString("DateTimeOriginal")
|
||||
if err == nil {
|
||||
log.Printf("Date shot: %s", date)
|
||||
layout := "2006:01:02 15:04:05"
|
||||
dateTime, err := time.Parse(layout, date)
|
||||
if err == nil {
|
||||
|
@ -65,68 +59,54 @@ func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models.
|
|||
// Get exposure time
|
||||
exposureTime, err := fileInfo.GetFloat("ExposureTime")
|
||||
if err == nil {
|
||||
log.Printf("Exposure time: %f", exposureTime)
|
||||
newExif.Exposure = &exposureTime
|
||||
}
|
||||
|
||||
// Get aperture
|
||||
aperture, err := fileInfo.GetFloat("Aperture")
|
||||
if err == nil {
|
||||
log.Printf("Aperture: %f", aperture)
|
||||
newExif.Aperture = &aperture
|
||||
}
|
||||
|
||||
// Get ISO
|
||||
iso, err := fileInfo.GetInt("ISO")
|
||||
if err == nil {
|
||||
log.Printf("ISO: %d", iso)
|
||||
newExif.Iso = &iso
|
||||
}
|
||||
|
||||
// Get focal length
|
||||
focalLen, err := fileInfo.GetString("FocalLength")
|
||||
focalLen, err := fileInfo.GetFloat("FocalLength")
|
||||
if err == nil {
|
||||
log.Printf("Focal length: %s", focalLen)
|
||||
reg, _ := regexp.Compile("[0-9.]+")
|
||||
focalLenStr := reg.FindString(focalLen)
|
||||
focalLenFloat, err := strconv.ParseFloat(focalLenStr, 64)
|
||||
if err == nil {
|
||||
newExif.FocalLength = &focalLenFloat
|
||||
}
|
||||
newExif.FocalLength = &focalLen
|
||||
}
|
||||
|
||||
// Get flash info
|
||||
flash, err := fileInfo.GetInt("Flash")
|
||||
if err == nil {
|
||||
log.Printf("Flash: %d", flash)
|
||||
newExif.Flash = &flash
|
||||
}
|
||||
|
||||
// Get orientation
|
||||
orientation, err := fileInfo.GetInt("Orientation")
|
||||
if err == nil {
|
||||
log.Printf("Orientation: %d", orientation)
|
||||
newExif.Orientation = &orientation
|
||||
}
|
||||
|
||||
// Get exposure program
|
||||
expProgram, err := fileInfo.GetInt("ExposureProgram")
|
||||
if err == nil {
|
||||
log.Printf("Exposure Program: %d", expProgram)
|
||||
newExif.ExposureProgram = &expProgram
|
||||
}
|
||||
|
||||
// GPS coordinates - longitude
|
||||
longitudeRaw, err := fileInfo.GetFloat("GPSLongitude")
|
||||
if err == nil {
|
||||
log.Printf("GPS longitude: %f", longitudeRaw)
|
||||
newExif.GPSLongitude = &longitudeRaw
|
||||
}
|
||||
|
||||
// GPS coordinates - latitude
|
||||
latitudeRaw, err := fileInfo.GetFloat("GPSLatitude")
|
||||
if err == nil {
|
||||
log.Printf("GPS latitude: %f", latitudeRaw)
|
||||
newExif.GPSLatitude = &latitudeRaw
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
"github.com/photoview/photoview/api/graphql/notification"
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sabhiram/go-gitignore"
|
||||
ignore "github.com/sabhiram/go-gitignore"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
|
@ -23,6 +23,9 @@ func getPhotoviewIgnore(ignorePath string) ([]string, error) {
|
|||
// Open .photoviewignore file, if exists
|
||||
photoviewIgnoreFile, err := os.Open(path.Join(ignorePath, ".photoviewignore"))
|
||||
if err != nil {
|
||||
if err == os.ErrNotExist {
|
||||
return photoviewIgnore, nil
|
||||
}
|
||||
return photoviewIgnore, err
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/photoview/photoview/api/graphql/dataloader"
|
||||
"github.com/photoview/photoview/api/routes"
|
||||
"github.com/photoview/photoview/api/scanner"
|
||||
"github.com/photoview/photoview/api/scanner/exif"
|
||||
"github.com/photoview/photoview/api/scanner/face_detection"
|
||||
"github.com/photoview/photoview/api/server"
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
|
@ -55,6 +56,8 @@ func main() {
|
|||
|
||||
scanner.InitializeExecutableWorkers()
|
||||
|
||||
exif.InitializeEXIFParser()
|
||||
|
||||
if err := face_detection.InitializeFaceDetector(db); err != nil {
|
||||
log.Panicf("Could not initialize face detector: %s\n", err)
|
||||
}
|
||||
|
@ -68,7 +71,8 @@ func main() {
|
|||
|
||||
graphqlResolver := resolvers.Resolver{Database: db}
|
||||
graphqlDirective := photoview_graphql.DirectiveRoot{}
|
||||
graphqlDirective.IsAdmin = photoview_graphql.IsAdmin(db)
|
||||
graphqlDirective.IsAdmin = photoview_graphql.IsAdmin
|
||||
graphqlDirective.IsAuthorized = photoview_graphql.IsAuthorized
|
||||
|
||||
graphqlConfig := photoview_graphql.Config{
|
||||
Resolvers: &graphqlResolver,
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
env: {
|
||||
browser: 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: {
|
||||
Atomics: 'readonly',
|
||||
SharedArrayBuffer: 'readonly',
|
||||
|
@ -18,17 +27,20 @@ module.exports = {
|
|||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['react', 'react-hooks'],
|
||||
plugins: ['react', 'react-hooks', '@typescript-eslint'],
|
||||
rules: {
|
||||
'no-unused-vars': 'warn',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'react/display-name': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
parser: 'babel-eslint',
|
||||
// parser: 'babel-eslint',
|
||||
overrides: [
|
||||
Object.assign(require('eslint-plugin-jest').configs.recommended, {
|
||||
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',
|
||||
},
|
||||
},
|
||||
}
|
|
@ -1,16 +1,30 @@
|
|||
module.exports = {
|
||||
presets: ['@babel/preset-env', '@babel/preset-react'],
|
||||
plugins: [
|
||||
'styled-components',
|
||||
'@babel/plugin-transform-runtime',
|
||||
'@babel/plugin-transform-modules-commonjs',
|
||||
'graphql-tag',
|
||||
// [
|
||||
// 'transform-semantic-ui-react-imports',
|
||||
// {
|
||||
// convertMemberImports: true,
|
||||
// addCssImports: true,
|
||||
// },
|
||||
// ],
|
||||
],
|
||||
module.exports = function (api) {
|
||||
const isTest = api.env('test')
|
||||
const isProduction = api.env('NODE_ENV') == 'production'
|
||||
|
||||
let presets = ['@babel/preset-react', '@babel/preset-typescript']
|
||||
let plugins = []
|
||||
|
||||
if (isTest) {
|
||||
presets.push('@babel/preset-env')
|
||||
plugins.push('@babel/plugin-transform-runtime')
|
||||
} else {
|
||||
if (!isProduction) {
|
||||
plugins.push([
|
||||
'i18next-extract',
|
||||
{
|
||||
locales: ['en', 'da'],
|
||||
defaultValue: null,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
plugins.push(['styled-components', { pure: true }])
|
||||
plugins.push('graphql-tag')
|
||||
}
|
||||
|
||||
return {
|
||||
presets: presets,
|
||||
plugins: plugins,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
const fs = require('fs-extra')
|
||||
const esbuild = require('esbuild')
|
||||
const bs = require('browser-sync').create()
|
||||
const historyApiFallback = require('connect-history-api-fallback')
|
||||
import fs from 'fs-extra'
|
||||
import esbuild from 'esbuild'
|
||||
import babel from 'esbuild-plugin-babel'
|
||||
import browserSync from 'browser-sync'
|
||||
import historyApiFallback from 'connect-history-api-fallback'
|
||||
import dotenv from 'dotenv'
|
||||
import workboxBuild from 'workbox-build'
|
||||
|
||||
require('dotenv').config()
|
||||
dotenv.config()
|
||||
const bs = browserSync.create()
|
||||
|
||||
const production = process.env.NODE_ENV == 'production'
|
||||
const watchMode = process.argv[2] == 'watch'
|
||||
|
@ -16,7 +20,12 @@ const defineEnv = ENVIRONMENT_VARIABLES.reduce((acc, key) => {
|
|||
}, {})
|
||||
|
||||
const esbuildOptions = {
|
||||
entryPoints: ['src/index.js'],
|
||||
entryPoints: ['src/index.tsx'],
|
||||
plugins: [
|
||||
babel({
|
||||
filter: /photoview\/ui\/src\/.*\.(js|tsx?)$/,
|
||||
}),
|
||||
],
|
||||
publicPath: process.env.UI_PUBLIC_URL || '/',
|
||||
outdir: 'dist',
|
||||
format: 'esm',
|
||||
|
@ -57,17 +66,25 @@ if (watchMode) {
|
|||
open: false,
|
||||
})
|
||||
|
||||
bs.watch('src/**/*.js').on('change', async args => {
|
||||
bs.watch('src/**/*.@(js|tsx|ts)').on('change', async args => {
|
||||
console.log('reloading', args)
|
||||
builderPromise = (await builderPromise).rebuild()
|
||||
bs.reload(args)
|
||||
// bs.reload(args)
|
||||
})
|
||||
} else {
|
||||
esbuild.buildSync(esbuildOptions)
|
||||
const build = async () => {
|
||||
await esbuild.build(esbuildOptions)
|
||||
|
||||
require('workbox-build').generateSW({
|
||||
globDirectory: 'dist/',
|
||||
globPatterns: ['**/*.{png,svg,woff2,ttf,eot,woff,js,ico,html,json,css}'],
|
||||
swDest: 'dist/service-worker.js',
|
||||
})
|
||||
console.log('esbuild done')
|
||||
|
||||
await workboxBuild.generateSW({
|
||||
globDirectory: 'dist/',
|
||||
globPatterns: ['**/*.{png,svg,woff2,ttf,eot,woff,js,ico,html,json,css}'],
|
||||
swDest: 'dist/service-worker.js',
|
||||
})
|
||||
|
||||
console.log('workbox done')
|
||||
console.log('build complete')
|
||||
}
|
||||
build()
|
||||
}
|
|
@ -0,0 +1,249 @@
|
|||
{
|
||||
"album_filter": {
|
||||
"only_favorites": "Vis kun favoritter",
|
||||
"sort_by": "Sorter efter",
|
||||
"sorting_options": {
|
||||
"date_imported": "Dato for importering",
|
||||
"date_shot": "Dato",
|
||||
"title": "Titel",
|
||||
"type": "Type"
|
||||
}
|
||||
},
|
||||
"albums_page": {
|
||||
"title": "Album"
|
||||
},
|
||||
"general": {
|
||||
"action": {
|
||||
"add": "Tilføj",
|
||||
"cancel": "Annuller",
|
||||
"delete": "Slet",
|
||||
"remove": "Fjern",
|
||||
"save": "Gem"
|
||||
},
|
||||
"loading": {
|
||||
"album": "Loader album",
|
||||
"default": "Loader...",
|
||||
"media": "Loader medier",
|
||||
"page": "Loader side",
|
||||
"paginate": {
|
||||
"faces": "Loader flere personer",
|
||||
"media": "Loader flere medier"
|
||||
},
|
||||
"shares": "Loader delinger...",
|
||||
"timeline": "Loader tidslinje"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
"search": {
|
||||
"loading": "Loader resultater...",
|
||||
"no_results": "Fandt ingen resultater",
|
||||
"placeholder": "Søg",
|
||||
"result_type": {
|
||||
"albums": "Albums",
|
||||
"photos": "Billeder"
|
||||
}
|
||||
}
|
||||
},
|
||||
"login_page": {
|
||||
"field": {
|
||||
"password": "Adgangskode",
|
||||
"submit": "Log ind",
|
||||
"username": "Brugernavn"
|
||||
},
|
||||
"initial_setup": {
|
||||
"field": {
|
||||
"photo_path": {
|
||||
"label": "Billedesti",
|
||||
"placeholder": "/sti/til/billeder"
|
||||
},
|
||||
"submit": "Opsæt Photoview"
|
||||
},
|
||||
"title": "Førstegangsopsætning"
|
||||
},
|
||||
"welcome": "Velkommen til Photoview"
|
||||
},
|
||||
"meta": {
|
||||
"description": "Simpelt og Brugervenligt Photo-galleri for Personlige Servere"
|
||||
},
|
||||
"people_page": {
|
||||
"face_group": {
|
||||
"label_placeholder": "Navn",
|
||||
"unlabeled": "Ikke navngivet"
|
||||
},
|
||||
"recognize_unlabeled_faces_button": "Genkend ikke navngivede ansigter"
|
||||
},
|
||||
"photos_page": {
|
||||
"title": "Billeder"
|
||||
},
|
||||
"routes": {
|
||||
"page_not_found": "Side ikke fundet"
|
||||
},
|
||||
"settings": {
|
||||
"concurrent_workers": {
|
||||
"description": "Det maksimale antal medier som må skannes samtidig",
|
||||
"title": "Samtidige scanner-arbejdere"
|
||||
},
|
||||
"logout": "Log ud",
|
||||
"periodic_scanner": {
|
||||
"checkbox_label": "Aktiver periodiske scanner",
|
||||
"field": {
|
||||
"description": "Hvor ofte scanneren bør udføre automatiske scanninger af alle brugere",
|
||||
"label": "Periodiske scanningsintervaller"
|
||||
},
|
||||
"interval_unit": {
|
||||
"days": "Dage",
|
||||
"hour": "Timer",
|
||||
"minutes": "Minutter",
|
||||
"months": "Måneder",
|
||||
"seconds": "Sekunder"
|
||||
},
|
||||
"title": "Periodisk scanner"
|
||||
},
|
||||
"scanner": {
|
||||
"description": "Vil scanne alle brugere for nye eller opdaterede medier",
|
||||
"scan_all_users": "Scan alle brugere",
|
||||
"title": "Scanner"
|
||||
},
|
||||
"user_preferences": {
|
||||
"change_language": {
|
||||
"description": "Set sidens sprog specifikt for denne bruger",
|
||||
"label": "Sprog"
|
||||
},
|
||||
"language_selector": {
|
||||
"placeholder": "Vælg sprog"
|
||||
},
|
||||
"title": "Brugerindstillinger"
|
||||
},
|
||||
"users": {
|
||||
"add_user": {
|
||||
"submit": "Tilføj bruger"
|
||||
},
|
||||
"confirm_delete_user": {
|
||||
"action": "Slet {user}",
|
||||
"description": "<0>Er du sikker på at du vil slette <1></1>?</0><p>Denne handling kan ikke fortrydes</p>",
|
||||
"title": "Slet bruger"
|
||||
},
|
||||
"password_reset": {
|
||||
"description": "Ændre adgangskode for <1></1>",
|
||||
"form": {
|
||||
"label": "Ny adgangskode",
|
||||
"placeholder": "adgangskode",
|
||||
"submit": "Ændre adgangskode"
|
||||
},
|
||||
"title": "Ændre adgangskode"
|
||||
},
|
||||
"table": {
|
||||
"column_names": {
|
||||
"action": "Handling",
|
||||
"admin": "Admin",
|
||||
"photo_path": "Billedesti",
|
||||
"username": "Brugernavn"
|
||||
},
|
||||
"new_user": "Ny bruger",
|
||||
"row": {
|
||||
"action": {
|
||||
"change_password": "Ændre adgangskode",
|
||||
"delete": "Slet",
|
||||
"edit": "Rediger",
|
||||
"scan": "Scan"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Brugere"
|
||||
}
|
||||
},
|
||||
"share_page": {
|
||||
"media": {
|
||||
"title": "Delt medie"
|
||||
},
|
||||
"protected_share": {
|
||||
"description": "Denne deling er låst med en adgangskode.",
|
||||
"title": "Beskyttet deling"
|
||||
},
|
||||
"share_not_found": "Deling blev ikke fundet",
|
||||
"share_not_found_description": "Måske er delingen udløbet eller blevet slettet.",
|
||||
"wrong_password": "Forkert adgangskode, prøv venligst igen."
|
||||
},
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"title": "Album indstillinger"
|
||||
},
|
||||
"download": {
|
||||
"filesize": {
|
||||
"byte": "{{count}} Byte",
|
||||
"byte_plural": "{{count}} Bytes",
|
||||
"giga_byte": "{{count}} GB",
|
||||
"kilo_byte": "{{count}} KB",
|
||||
"mega_byte": "{{count}} MB",
|
||||
"tera_byte": "{{count}} TB"
|
||||
},
|
||||
"table_columns": {
|
||||
"dimensions": "Dimension",
|
||||
"file_size": "Størrelse",
|
||||
"file_type": "Type",
|
||||
"name": "Navn"
|
||||
},
|
||||
"title": "Download"
|
||||
},
|
||||
"media": {
|
||||
"exif": {
|
||||
"exposure_program": {
|
||||
"action_program": "Actionprogram",
|
||||
"aperture_priority": "Blændeprioritet",
|
||||
"bulb": "Bulb",
|
||||
"creative_program": "Kreativ program",
|
||||
"landscape_mode": "Landskabsmode",
|
||||
"manual": "Manuel",
|
||||
"normal_program": "Normal program",
|
||||
"not_defined": "Ikke defineret",
|
||||
"portrait_mode": "Portræt mode",
|
||||
"shutter_priority": "Lukkerprioritet"
|
||||
},
|
||||
"flash": {
|
||||
"auto": "Auto",
|
||||
"did_not_fire": "Blitz affyrede ikke",
|
||||
"fired": "Affyrede",
|
||||
"no_flash": "Ingen blitz",
|
||||
"no_flash_function": "Ingen blitz-funktion",
|
||||
"off": "Slukket",
|
||||
"on": "Tændt",
|
||||
"red_eye_reduction": "Røde øjne reduktion",
|
||||
"return_detected": "Retur registreret",
|
||||
"return_not_detected": "Retur ikke registreret"
|
||||
},
|
||||
"name": {
|
||||
"aperture": "Blænde",
|
||||
"camera": "Kamera",
|
||||
"date_shot": "Dato",
|
||||
"exposure": "Lukketid",
|
||||
"exposure_program": "Lukketid program",
|
||||
"flash": "Blitz",
|
||||
"focal_length": "Brændvidde",
|
||||
"iso": "ISO",
|
||||
"lens": "Lense",
|
||||
"maker": "Mærke"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sharing": {
|
||||
"add_share": "Tilføj deling",
|
||||
"copy_link": "Kopier link",
|
||||
"no_shares_found": "Ingen delinger fundet",
|
||||
"public_link": "Offentligt link",
|
||||
"table_header": "Offentlige delinger",
|
||||
"title": "Indstillinger for deling"
|
||||
}
|
||||
},
|
||||
"sidemenu": {
|
||||
"albums": "Albums",
|
||||
"people": "Personer",
|
||||
"photos": "Billeder",
|
||||
"places": "Kort",
|
||||
"settings": "Indstillinger"
|
||||
},
|
||||
"title": {
|
||||
"loading_album": "Loader album",
|
||||
"people": "Personer",
|
||||
"settings": "Indstillinger"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,249 @@
|
|||
{
|
||||
"album_filter": {
|
||||
"only_favorites": "Show only favorites",
|
||||
"sort_by": "Sort by",
|
||||
"sorting_options": {
|
||||
"date_imported": "Date imported",
|
||||
"date_shot": "Date shot",
|
||||
"title": "Title",
|
||||
"type": "Kind"
|
||||
}
|
||||
},
|
||||
"albums_page": {
|
||||
"title": "Albums"
|
||||
},
|
||||
"general": {
|
||||
"action": {
|
||||
"add": "Add",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"remove": "Remove",
|
||||
"save": "Save"
|
||||
},
|
||||
"loading": {
|
||||
"album": "Loading album",
|
||||
"default": "Loading...",
|
||||
"media": "Loading media",
|
||||
"page": "Loading page",
|
||||
"paginate": {
|
||||
"faces": "Loading more people",
|
||||
"media": "Loading more media"
|
||||
},
|
||||
"shares": "Loading shares...",
|
||||
"timeline": "Loading timeline"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
"search": {
|
||||
"loading": "Loading results...",
|
||||
"no_results": "No results found",
|
||||
"placeholder": "Search",
|
||||
"result_type": {
|
||||
"albums": "Albums",
|
||||
"photos": "Photos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"login_page": {
|
||||
"field": {
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"username": "Username"
|
||||
},
|
||||
"initial_setup": {
|
||||
"field": {
|
||||
"photo_path": {
|
||||
"label": "Photo path",
|
||||
"placeholder": "/path/to/photos"
|
||||
},
|
||||
"submit": "Setup Photoview"
|
||||
},
|
||||
"title": "Initial Setup"
|
||||
},
|
||||
"welcome": "Welcome to Photoview"
|
||||
},
|
||||
"meta": {
|
||||
"description": "Simple and User-friendly Photo Gallery for Personal Servers"
|
||||
},
|
||||
"people_page": {
|
||||
"face_group": {
|
||||
"label_placeholder": "Label",
|
||||
"unlabeled": "Unlabeled"
|
||||
},
|
||||
"recognize_unlabeled_faces_button": "Recognize unlabeled faces"
|
||||
},
|
||||
"photos_page": {
|
||||
"title": "Photos"
|
||||
},
|
||||
"routes": {
|
||||
"page_not_found": "Page not found"
|
||||
},
|
||||
"settings": {
|
||||
"concurrent_workers": {
|
||||
"description": "The maximum amount of scanner jobs that is allowed to run at once",
|
||||
"title": "Scanner concurrent workers"
|
||||
},
|
||||
"logout": "Log out",
|
||||
"periodic_scanner": {
|
||||
"checkbox_label": "Enable periodic scanner",
|
||||
"field": {
|
||||
"description": "How often the scanner should perform automatic scans of all users",
|
||||
"label": "Periodic scan interval"
|
||||
},
|
||||
"interval_unit": {
|
||||
"days": "Days",
|
||||
"hour": "Hour",
|
||||
"minutes": "Minutes",
|
||||
"months": "Months",
|
||||
"seconds": "Seconds"
|
||||
},
|
||||
"title": "Periodic scanner"
|
||||
},
|
||||
"scanner": {
|
||||
"description": "Will scan all users for new or updated media",
|
||||
"scan_all_users": "Scan all users",
|
||||
"title": "Scanner"
|
||||
},
|
||||
"user_preferences": {
|
||||
"change_language": {
|
||||
"description": "Change website language specific for this user",
|
||||
"label": "Website language"
|
||||
},
|
||||
"language_selector": {
|
||||
"placeholder": "Select language"
|
||||
},
|
||||
"title": "User preferences"
|
||||
},
|
||||
"users": {
|
||||
"add_user": {
|
||||
"submit": "Add user"
|
||||
},
|
||||
"confirm_delete_user": {
|
||||
"action": "Delete {{user}}",
|
||||
"description": "<0>Are you sure, you want to delete <1></1>?</0><p>This action cannot be undone</p>",
|
||||
"title": "Delete user"
|
||||
},
|
||||
"password_reset": {
|
||||
"description": "Change password for <1></1>",
|
||||
"form": {
|
||||
"label": "New password",
|
||||
"placeholder": "password",
|
||||
"submit": "Change password"
|
||||
},
|
||||
"title": "Change password"
|
||||
},
|
||||
"table": {
|
||||
"column_names": {
|
||||
"action": "Action",
|
||||
"admin": "Admin",
|
||||
"photo_path": "Photo path",
|
||||
"username": "Username"
|
||||
},
|
||||
"new_user": "New user",
|
||||
"row": {
|
||||
"action": {
|
||||
"change_password": "Change password",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"scan": "Scan"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Users"
|
||||
}
|
||||
},
|
||||
"share_page": {
|
||||
"media": {
|
||||
"title": "Shared media"
|
||||
},
|
||||
"protected_share": {
|
||||
"description": "This share is protected with a password.",
|
||||
"title": "Protected share"
|
||||
},
|
||||
"share_not_found": "Share not found",
|
||||
"share_not_found_description": "Maybe the share has expired or has been deleted.",
|
||||
"wrong_password": "Wrong password, please try again."
|
||||
},
|
||||
"sidebar": {
|
||||
"album": {
|
||||
"title": "Album options"
|
||||
},
|
||||
"download": {
|
||||
"filesize": {
|
||||
"byte": "{{count}} Byte",
|
||||
"byte_plural": "{{count}} Bytes",
|
||||
"giga_byte": "{{count}} GB",
|
||||
"kilo_byte": "{{count}} KB",
|
||||
"mega_byte": "{{count}} MB",
|
||||
"tera_byte": "{{count}} TB"
|
||||
},
|
||||
"table_columns": {
|
||||
"dimensions": "Dimensions",
|
||||
"file_size": "Size",
|
||||
"file_type": "Type",
|
||||
"name": "Name"
|
||||
},
|
||||
"title": "Download"
|
||||
},
|
||||
"media": {
|
||||
"exif": {
|
||||
"exposure_program": {
|
||||
"action_program": "Action program",
|
||||
"aperture_priority": "Aperture priority",
|
||||
"bulb": "Bulb",
|
||||
"creative_program": "Creative program",
|
||||
"landscape_mode": "Landscape mode",
|
||||
"manual": "Manual",
|
||||
"normal_program": "Normal program",
|
||||
"not_defined": "Not defined",
|
||||
"portrait_mode": "Portrait mode",
|
||||
"shutter_priority": "Shutter priority"
|
||||
},
|
||||
"flash": {
|
||||
"auto": "Auto",
|
||||
"did_not_fire": "Did not fire",
|
||||
"fired": "Fired",
|
||||
"no_flash": "No Flash",
|
||||
"no_flash_function": "No flash function",
|
||||
"off": "Off",
|
||||
"on": "On",
|
||||
"red_eye_reduction": "Red-eye reduction",
|
||||
"return_detected": "Return detected",
|
||||
"return_not_detected": "Return not detected"
|
||||
},
|
||||
"name": {
|
||||
"aperture": "Aperture",
|
||||
"camera": "Camera",
|
||||
"date_shot": "Date shot",
|
||||
"exposure": "Exposure",
|
||||
"exposure_program": "Program",
|
||||
"flash": "Flash",
|
||||
"focal_length": "Focal length",
|
||||
"iso": "ISO",
|
||||
"lens": "Lens",
|
||||
"maker": "Maker"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sharing": {
|
||||
"add_share": "Add shares",
|
||||
"copy_link": "Copy Link",
|
||||
"no_shares_found": "No shares found",
|
||||
"public_link": "Public Link",
|
||||
"table_header": "Public shares",
|
||||
"title": "Sharing options"
|
||||
}
|
||||
},
|
||||
"sidemenu": {
|
||||
"albums": "Albums",
|
||||
"people": "People",
|
||||
"photos": "Photos",
|
||||
"places": "Places",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"title": {
|
||||
"loading_album": "Loading album",
|
||||
"people": "People",
|
||||
"settings": "Settings"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -9,15 +9,14 @@
|
|||
"license": "GPL-3.0",
|
||||
"description": "UI app for Photoview",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.3.13",
|
||||
"@babel/core": "^7.13.14",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.13.8",
|
||||
"@babel/plugin-transform-runtime": "^7.13.10",
|
||||
"@babel/preset-env": "^7.13.12",
|
||||
"@apollo/client": "^3.3.14",
|
||||
"@babel/core": "^7.13.15",
|
||||
"@babel/preset-react": "^7.13.13",
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babel-plugin-graphql-tag": "^3.2.0",
|
||||
"babel-plugin-i18next-extract": "^0.8.3",
|
||||
"babel-plugin-styled-components": "^1.12.0",
|
||||
"babel-plugin-transform-semantic-ui-react-imports": "^1.4.1",
|
||||
"browser-sync": "^2.26.14",
|
||||
|
@ -25,18 +24,20 @@
|
|||
"copy-to-clipboard": "^3.3.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"esbuild": "^0.8.52",
|
||||
"eslint": "^7.23.0",
|
||||
"eslint-plugin-jest": "^24.3.3",
|
||||
"eslint-plugin-jest-dom": "^3.7.0",
|
||||
"eslint-plugin-react": "^7.23.1",
|
||||
"esbuild-plugin-babel": "^0.2.3",
|
||||
"eslint": "^7.24.0",
|
||||
"eslint-plugin-jest": "^24.3.5",
|
||||
"eslint-plugin-jest-dom": "^3.8.0",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"fs-extra": "^9.1.0",
|
||||
"graphql": "^15.5.0",
|
||||
"i18next": "^20.2.1",
|
||||
"mapbox-gl": "^2.2.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-i18next": "^11.8.12",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-router-prop-types": "^1.0.5",
|
||||
"react-spring": "^8.0.27",
|
||||
|
@ -45,25 +46,41 @@
|
|||
"semantic-ui-react": "^2.0.3",
|
||||
"styled-components": "^5.2.3",
|
||||
"subscriptions-transport-ws": "^0.9.18",
|
||||
"typescript": "^4.2.4",
|
||||
"url-join": "^4.0.1",
|
||||
"workbox-build": "^6.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node build.js watch",
|
||||
"build": "NODE_ENV=production node build.js",
|
||||
"start": "node --experimental-modules build.mjs watch",
|
||||
"build": "NODE_ENV=production node --experimental-modules build.mjs",
|
||||
"test": "npm run lint && npm run jest",
|
||||
"lint": "eslint ./src --max-warnings 0 --cache",
|
||||
"jest": "jest",
|
||||
"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 --verbose",
|
||||
"genSchemaTypes": "npx apollo client:codegen --target=typescript",
|
||||
"prepare": "(cd .. && npx husky install)"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-transform-runtime": "^7.13.15",
|
||||
"@babel/preset-env": "^7.13.15",
|
||||
"@testing-library/jest-dom": "^5.11.10",
|
||||
"@testing-library/react": "^11.2.6",
|
||||
"@types/jest": "^26.0.22",
|
||||
"@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",
|
||||
"jest": "^26.6.3",
|
||||
"lint-staged": "^10.5.4",
|
||||
"prettier": "^2.2.1"
|
||||
"prettier": "^2.2.1",
|
||||
"tsc-files": "^1.1.2"
|
||||
},
|
||||
"cache": {
|
||||
"swDest": "service-worker.js"
|
||||
|
@ -80,12 +97,13 @@
|
|||
"^.+\\.css$"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.js$": "babel-jest",
|
||||
"^.+\\.(js|ts|tsx)$": "babel-jest",
|
||||
"^.+\\.svg$": "<rootDir>/testing/transform-svg.js"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,json,css,md,graphql}": "prettier --write",
|
||||
"*.js": "eslint --cache --fix --max-warnings 0"
|
||||
"*.{ts,tsx,js,json,css,md,graphql}": "prettier --write",
|
||||
"*.{js,ts,tsx}": "eslint --cache --fix --max-warnings 0",
|
||||
"*.{ts,tsx}": "tsc-files --noEmit"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
declare module '*.svg' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
import React from 'react'
|
||||
import { createGlobalStyle } from 'styled-components'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import Routes from './components/routes/Routes'
|
||||
import Messages from './components/messages/Messages'
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
#root, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
/* Make dimmer lighter */
|
||||
.ui.dimmer {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
`
|
||||
|
||||
import 'semantic-ui-css/semantic.min.css'
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<meta
|
||||
name="description"
|
||||
content="Simple and User-friendly Photo Gallery for Personal Servers"
|
||||
/>
|
||||
</Helmet>
|
||||
<GlobalStyle />
|
||||
<Routes />
|
||||
<Messages />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
|
@ -0,0 +1,101 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import { createGlobalStyle } from 'styled-components'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import Routes from './components/routes/Routes'
|
||||
import Messages from './components/messages/Messages'
|
||||
import i18n from 'i18next'
|
||||
import { gql, useLazyQuery } from '@apollo/client'
|
||||
import { authToken } from './helpers/authentication'
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
#root, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
/* Make dimmer lighter */
|
||||
.ui.dimmer {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
`
|
||||
|
||||
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`
|
||||
query siteTranslation {
|
||||
myUserPreferences {
|
||||
id
|
||||
language
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const loadTranslations = () => {
|
||||
console.log('load translation')
|
||||
const [loadLang, { data }] = useLazyQuery<siteTranslation>(SITE_TRANSLATION)
|
||||
|
||||
useEffect(() => {
|
||||
if (authToken()) {
|
||||
loadLang()
|
||||
}
|
||||
}, [authToken()])
|
||||
|
||||
useEffect(() => {
|
||||
console.log('loading translations', data)
|
||||
switch (data?.myUserPreferences.language) {
|
||||
case LanguageTranslation.Danish:
|
||||
import('../extractedTranslations/da/translation.json').then(danish => {
|
||||
console.log('loading danish')
|
||||
i18n.addResourceBundle('da', 'translation', danish)
|
||||
i18n.changeLanguage('da')
|
||||
})
|
||||
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:
|
||||
i18n.changeLanguage('en')
|
||||
}
|
||||
}, [data?.myUserPreferences.language])
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const { t } = useTranslation()
|
||||
loadTranslations()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<meta
|
||||
name="description"
|
||||
content={t(
|
||||
'meta.description',
|
||||
'Simple and User-friendly Photo Gallery for Personal Servers'
|
||||
)}
|
||||
/>
|
||||
</Helmet>
|
||||
<GlobalStyle />
|
||||
<Routes />
|
||||
<Messages />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
|
@ -9,7 +9,9 @@ import { MemoryRouter } from 'react-router-dom'
|
|||
|
||||
import * as authentication from './helpers/authentication'
|
||||
|
||||
jest.mock('./helpers/authentication.js')
|
||||
require('./localization').default()
|
||||
|
||||
jest.mock('./helpers/authentication.ts')
|
||||
|
||||
test('Layout component', async () => {
|
||||
render(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import React, { ReactChild } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled from 'styled-components'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
|
@ -9,6 +9,7 @@ import { Authorized } from './components/routes/AuthorizedRoute'
|
|||
import { Helmet } from 'react-helmet'
|
||||
import Header from './components/header/Header'
|
||||
import { authToken } from './helpers/authentication'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const ADMIN_QUERY = gql`
|
||||
query adminQuery {
|
||||
|
@ -78,16 +79,28 @@ const SideButtonLink = styled(NavLink)`
|
|||
}
|
||||
`
|
||||
|
||||
const SideButton = props => {
|
||||
return (
|
||||
<SideButtonLink {...props} activeStyle={{ color: '#4183c4' }}>
|
||||
{props.children}
|
||||
</SideButtonLink>
|
||||
)
|
||||
type SideButtonProps = {
|
||||
children: ReactChild | ReactChild[]
|
||||
to: string
|
||||
exact: boolean
|
||||
}
|
||||
|
||||
SideButton.propTypes = {
|
||||
children: PropTypes.any,
|
||||
const SideButton = ({
|
||||
children,
|
||||
to,
|
||||
exact,
|
||||
...otherProps
|
||||
}: SideButtonProps) => {
|
||||
return (
|
||||
<SideButtonLink
|
||||
{...otherProps}
|
||||
to={to}
|
||||
exact={exact}
|
||||
activeStyle={{ color: '#4183c4' }}
|
||||
>
|
||||
{children}
|
||||
</SideButtonLink>
|
||||
)
|
||||
}
|
||||
|
||||
const SideButtonLabel = styled.div`
|
||||
|
@ -95,47 +108,46 @@ const SideButtonLabel = styled.div`
|
|||
`
|
||||
|
||||
export const SideMenu = () => {
|
||||
const adminQuery = authToken() ? useQuery(ADMIN_QUERY) : null
|
||||
const { t } = useTranslation()
|
||||
|
||||
const mapboxQuery = authToken() ? useQuery(MAPBOX_QUERY) : null
|
||||
|
||||
const isAdmin = adminQuery?.data?.myUser?.admin
|
||||
const mapboxEnabled = !!mapboxQuery?.data?.mapboxToken
|
||||
|
||||
return (
|
||||
<SideMenuContainer>
|
||||
<SideButton to="/photos" exact>
|
||||
<Icon name="image" />
|
||||
<SideButtonLabel>Photos</SideButtonLabel>
|
||||
<SideButtonLabel>{t('sidemenu.photos', 'Photos')}</SideButtonLabel>
|
||||
</SideButton>
|
||||
<SideButton to="/albums" exact>
|
||||
<Icon name="images" />
|
||||
<SideButtonLabel>Albums</SideButtonLabel>
|
||||
<SideButtonLabel>{t('sidemenu.albums', 'Albums')}</SideButtonLabel>
|
||||
</SideButton>
|
||||
{mapboxEnabled ? (
|
||||
<SideButton to="/places" exact>
|
||||
<Icon name="map" />
|
||||
<SideButtonLabel>Places</SideButtonLabel>
|
||||
<SideButtonLabel>{t('sidemenu.places', 'Places')}</SideButtonLabel>
|
||||
</SideButton>
|
||||
) : null}
|
||||
<SideButton to="/people" exact>
|
||||
<Icon name="user" />
|
||||
<SideButtonLabel>People</SideButtonLabel>
|
||||
<SideButtonLabel>{t('sidemenu.people', 'People')}</SideButtonLabel>
|
||||
</SideButton>
|
||||
{isAdmin ? (
|
||||
<SideButton to="/settings" exact>
|
||||
<Icon name="settings" />
|
||||
<SideButtonLabel>Settings</SideButtonLabel>
|
||||
</SideButton>
|
||||
) : null}
|
||||
<SideButton to="/logout">
|
||||
<Icon name="lock" />
|
||||
<SideButtonLabel>Log out</SideButtonLabel>
|
||||
<SideButton to="/settings" exact>
|
||||
<Icon name="settings" />
|
||||
<SideButtonLabel>{t('sidemenu.settings', 'Settings')}</SideButtonLabel>
|
||||
</SideButton>
|
||||
</SideMenuContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const Layout = ({ children, title, ...otherProps }) => {
|
||||
type LayoutProps = {
|
||||
children: React.ReactNode
|
||||
title: string
|
||||
}
|
||||
|
||||
const Layout = ({ children, title, ...otherProps }: LayoutProps) => {
|
||||
return (
|
||||
<Container {...otherProps} data-testid="Layout">
|
||||
<Helmet>
|
|
@ -1,15 +1,16 @@
|
|||
import React, { useCallback, useEffect } from 'react'
|
||||
import ReactRouterPropTypes from 'react-router-prop-types'
|
||||
import { useQuery, gql } from '@apollo/client'
|
||||
import AlbumGallery from '../../components/albumGallery/AlbumGallery'
|
||||
import PropTypes from 'prop-types'
|
||||
import Layout from '../../Layout'
|
||||
import useURLParameters from '../../hooks/useURLParameters'
|
||||
import useURLParameters, { UrlKeyValuePair } from '../../hooks/useURLParameters'
|
||||
import useScrollPagination from '../../hooks/useScrollPagination'
|
||||
import PaginateLoader from '../../components/PaginateLoader'
|
||||
import LazyLoad from '../../helpers/LazyLoad'
|
||||
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(
|
||||
$id: ID!
|
||||
$onlyFavorites: Boolean
|
||||
|
@ -60,20 +61,39 @@ const albumQuery = gql`
|
|||
let refetchNeededAll = 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 { t } = useTranslation()
|
||||
|
||||
const { getParam, setParam, setParams } = useURLParameters()
|
||||
|
||||
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 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 }) => {
|
||||
let updatedParams = []
|
||||
const updatedParams: UrlKeyValuePair[] = []
|
||||
if (orderBy !== undefined) {
|
||||
updatedParams.push({ key: 'orderBy', value: orderBy })
|
||||
}
|
||||
|
@ -86,7 +106,10 @@ function AlbumPage({ match }) {
|
|||
[setParams]
|
||||
)
|
||||
|
||||
const { loading, error, data, refetch, fetchMore } = useQuery(albumQuery, {
|
||||
const { loading, error, data, refetch, fetchMore } = useQuery<
|
||||
albumQuery,
|
||||
albumQueryVariables
|
||||
>(ALBUM_QUERY, {
|
||||
variables: {
|
||||
id: albumId,
|
||||
onlyFavorites,
|
||||
|
@ -97,7 +120,10 @@ function AlbumPage({ match }) {
|
|||
},
|
||||
})
|
||||
|
||||
const { containerElem, finished: finishedLoadingMore } = useScrollPagination({
|
||||
const {
|
||||
containerElem,
|
||||
finished: finishedLoadingMore,
|
||||
} = useScrollPagination<albumQuery>({
|
||||
loading,
|
||||
fetchMore,
|
||||
data,
|
||||
|
@ -139,12 +165,15 @@ function AlbumPage({ match }) {
|
|||
if (error) return <div>Error</div>
|
||||
|
||||
return (
|
||||
<Layout title={data ? data.album.title : 'Loading album'}>
|
||||
<Layout
|
||||
title={
|
||||
data ? data.album.title : t('title.loading_album', 'Loading album')
|
||||
}
|
||||
>
|
||||
<AlbumGallery
|
||||
ref={containerElem}
|
||||
album={data && data.album}
|
||||
loading={loading}
|
||||
showFavoritesToggle
|
||||
setOnlyFavorites={toggleFavorites}
|
||||
onlyFavorites={onlyFavorites}
|
||||
onFavorite={() => (refetchNeededAll = refetchNeededFavorites = true)}
|
||||
|
@ -154,20 +183,10 @@ function AlbumPage({ match }) {
|
|||
/>
|
||||
<PaginateLoader
|
||||
active={!finishedLoadingMore && !loading}
|
||||
text="Loading more media"
|
||||
text={t('general.loading.paginate.media', 'Loading more media')}
|
||||
/>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
AlbumPage.propTypes = {
|
||||
...ReactRouterPropTypes,
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
subPage: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -3,6 +3,7 @@ import AlbumBoxes from '../../components/albumGallery/AlbumBoxes'
|
|||
import Layout from '../../Layout'
|
||||
import { useQuery, gql } from '@apollo/client'
|
||||
import LazyLoad from '../../helpers/LazyLoad'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const getAlbumsQuery = gql`
|
||||
query getMyAlbums {
|
||||
|
@ -19,6 +20,7 @@ const getAlbumsQuery = gql`
|
|||
`
|
||||
|
||||
const AlbumsPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const { loading, error, data } = useQuery(getAlbumsQuery)
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -31,7 +33,7 @@ const AlbumsPage = () => {
|
|||
|
||||
return (
|
||||
<Layout title="Albums">
|
||||
<h1>Albums</h1>
|
||||
<h1>{t('albums_page.title', 'Albums')}</h1>
|
||||
{!loading && (
|
||||
<AlbumBoxes
|
||||
loading={loading}
|
|
@ -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[];
|
||||
}
|
|
@ -6,6 +6,8 @@ import { Container } from './loginUtilities'
|
|||
|
||||
import { checkInitialSetupQuery, login } from './loginUtilities'
|
||||
import { authToken } from '../../helpers/authentication'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CheckInitialSetup } from './__generated__/CheckInitialSetup'
|
||||
|
||||
const initialSetupMutation = gql`
|
||||
mutation InitialSetup(
|
||||
|
@ -26,37 +28,21 @@ const initialSetupMutation = gql`
|
|||
`
|
||||
|
||||
const InitialSetupPage = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [state, setState] = useState({
|
||||
username: '',
|
||||
password: '',
|
||||
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()) {
|
||||
return <Redirect to="/" />
|
||||
}
|
||||
|
||||
const { data: initialSetupData } = useQuery(checkInitialSetupQuery)
|
||||
const { data: initialSetupData } = useQuery<CheckInitialSetup>(
|
||||
checkInitialSetupQuery
|
||||
)
|
||||
const initialSetupRedirect = initialSetupData?.siteInfo
|
||||
?.initialSetup ? null : (
|
||||
<Redirect to="/" />
|
||||
|
@ -75,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
|
||||
if (authorizationData && !authorizationData.initialSetupWizard.success) {
|
||||
errorMessage = authorizationData.initialSetupWizard.status
|
||||
|
@ -85,37 +94,47 @@ const InitialSetupPage = () => {
|
|||
{initialSetupRedirect}
|
||||
<Container>
|
||||
<Header as="h1" textAlign="center">
|
||||
Initial Setup
|
||||
{t('login_page.initial_setup.title', 'Initial Setup')}
|
||||
</Header>
|
||||
<Form
|
||||
style={{ width: 500, margin: 'auto' }}
|
||||
error={!!errorMessage}
|
||||
onSubmit={e => signIn(e, authorize)}
|
||||
onSubmit={signIn}
|
||||
loading={
|
||||
authorizeLoading || authorizationData?.initialSetupWizard?.success
|
||||
}
|
||||
>
|
||||
<Form.Field>
|
||||
<label>Username</label>
|
||||
<label>{t('login_page.field.username', 'Username')}</label>
|
||||
<input onChange={e => handleChange(e, 'username')} />
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>Password</label>
|
||||
<label>{t('login_page.field.password', 'Password')}</label>
|
||||
<input
|
||||
type="password"
|
||||
onChange={e => handleChange(e, 'password')}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>Photo Path</label>
|
||||
<label>
|
||||
{t(
|
||||
'login_page.initial_setup.field.photo_path.label',
|
||||
'Photo path'
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
placeholder="/path/to/photos"
|
||||
placeholder={t(
|
||||
'login_page.initial_setup.field.photo_path.placeholder',
|
||||
'/path/to/photos'
|
||||
)}
|
||||
type="text"
|
||||
onChange={e => handleChange(e, 'rootPath')}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Message error content={errorMessage} />
|
||||
<Button type="submit">Setup Photoview</Button>
|
||||
<Button type="submit">
|
||||
{t('login_page.initial_setup.field.submit', 'Setup Photoview')}
|
||||
</Button>
|
||||
</Form>
|
||||
</Container>
|
||||
</div>
|
|
@ -2,11 +2,12 @@ import React, { useState, useCallback } from 'react'
|
|||
import { useQuery, gql, useMutation } from '@apollo/client'
|
||||
import { Redirect } from 'react-router-dom'
|
||||
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 { authToken } from '../../helpers/authentication'
|
||||
|
||||
import logoPath from '../../assets/photoview-logo.svg'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const authorizeMutation = gql`
|
||||
mutation Authorize($username: String!, $password: String!) {
|
||||
|
@ -22,18 +23,26 @@ const StyledLogo = styled.img`
|
|||
max-height: 128px;
|
||||
`
|
||||
|
||||
const LogoHeader = props => (
|
||||
<Header {...props} as="h1" textAlign="center">
|
||||
<StyledLogo src={logoPath} alt="photoview logo" />
|
||||
<p style={{ fontWeight: 400 }}>Welcome to Photoview</p>
|
||||
</Header>
|
||||
)
|
||||
const LogoHeader = (props: HeaderProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Header {...props} as="h1" textAlign="center">
|
||||
<StyledLogo src={logoPath} alt="photoview logo" />
|
||||
<p style={{ fontWeight: 400 }}>
|
||||
{t('login_page.welcome', 'Welcome to Photoview')}
|
||||
</p>
|
||||
</Header>
|
||||
)
|
||||
}
|
||||
|
||||
const LogoHeaderStyled = styled(LogoHeader)`
|
||||
margin-bottom: 72px !important;
|
||||
`
|
||||
|
||||
const LoginPage = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [credentials, setCredentials] = useState({
|
||||
username: '',
|
||||
password: '',
|
||||
|
@ -99,14 +108,18 @@ const LoginPage = () => {
|
|||
loading={loading || (data && data.authorizeUser.success)}
|
||||
>
|
||||
<Form.Field>
|
||||
<label htmlFor="username_field">Username</label>
|
||||
<label htmlFor="username_field">
|
||||
{t('login_page.field.username', 'Username')}
|
||||
</label>
|
||||
<input
|
||||
id="username_field"
|
||||
onChange={e => handleChange(e, 'username')}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label htmlFor="password_field">Password</label>
|
||||
<label htmlFor="password_field">
|
||||
{t('login_page.field.password', 'Password')}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password_field"
|
||||
|
@ -114,7 +127,9 @@ const LoginPage = () => {
|
|||
/>
|
||||
</Form.Field>
|
||||
<Message error content={errorMessage} />
|
||||
<Button type="submit">Sign in</Button>
|
||||
<Button type="submit">
|
||||
{t('login_page.field.submit', 'Sign in')}
|
||||
</Button>
|
||||
</Form>
|
||||
</Container>
|
||||
</div>
|
|
@ -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)
|
||||
window.location = '/'
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
export const Container = styled(SemanticContainer)`
|
|
@ -9,6 +9,7 @@ import { Button, Icon, Input } from 'semantic-ui-react'
|
|||
import FaceCircleImage from './FaceCircleImage'
|
||||
import useScrollPagination from '../../hooks/useScrollPagination'
|
||||
import PaginateLoader from '../../components/PaginateLoader'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const MY_FACES_QUERY = gql`
|
||||
query myFaces($limit: Int, $offset: Int) {
|
||||
|
@ -38,7 +39,7 @@ export const MY_FACES_QUERY = gql`
|
|||
`
|
||||
|
||||
export const SET_GROUP_LABEL_MUTATION = gql`
|
||||
mutation($groupID: ID!, $label: String) {
|
||||
mutation setGroupLabel($groupID: ID!, $label: String) {
|
||||
setFaceGroupLabel(faceGroupID: $groupID, label: $label) {
|
||||
id
|
||||
label
|
||||
|
@ -73,6 +74,7 @@ const FaceDetailsButton = styled.button`
|
|||
const FaceLabel = styled.span``
|
||||
|
||||
const FaceDetails = ({ group }) => {
|
||||
const { t } = useTranslation()
|
||||
const [editLabel, setEditLabel] = useState(false)
|
||||
const [inputValue, setInputValue] = useState(group.label ?? '')
|
||||
const inputRef = createRef()
|
||||
|
@ -124,7 +126,9 @@ const FaceDetails = ({ group }) => {
|
|||
onClick={() => setEditLabel(true)}
|
||||
>
|
||||
<FaceImagesCount>{group.imageFaceCount}</FaceImagesCount>
|
||||
<FaceLabel>{group.label ?? 'Unlabeled'}</FaceLabel>
|
||||
<FaceLabel>
|
||||
{group.label ?? t('people_page.face_group.unlabeled', 'Unlabeled')}
|
||||
</FaceLabel>
|
||||
<EditIcon name="pencil" />
|
||||
</FaceDetailsButton>
|
||||
)
|
||||
|
@ -135,7 +139,7 @@ const FaceDetails = ({ group }) => {
|
|||
loading={loading}
|
||||
ref={inputRef}
|
||||
size="mini"
|
||||
placeholder="Label"
|
||||
placeholder={t('people_page.face_group.label_placeholder', 'Label')}
|
||||
icon="arrow right"
|
||||
value={inputValue}
|
||||
onKeyUp={onKeyUp}
|
||||
|
@ -199,6 +203,7 @@ const FaceGroupsWrapper = styled.div`
|
|||
`
|
||||
|
||||
const PeopleGallery = () => {
|
||||
const { t } = useTranslation()
|
||||
const { data, error, loading, fetchMore } = useQuery(MY_FACES_QUERY, {
|
||||
variables: {
|
||||
limit: 50,
|
||||
|
@ -230,7 +235,7 @@ const PeopleGallery = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Layout title={'People'}>
|
||||
<Layout title={t('title.people', 'People')}>
|
||||
<Button
|
||||
loading={recognizeUnlabeledLoading}
|
||||
disabled={recognizeUnlabeledLoading}
|
||||
|
@ -239,12 +244,15 @@ const PeopleGallery = () => {
|
|||
}}
|
||||
>
|
||||
<Icon name="sync" />
|
||||
Recognize unlabeled faces
|
||||
{t(
|
||||
'people_page.recognize_unlabeled_faces_button',
|
||||
'Recognize unlabeled faces'
|
||||
)}
|
||||
</Button>
|
||||
<FaceGroupsWrapper ref={containerElem}>{faces}</FaceGroupsWrapper>
|
||||
<PaginateLoader
|
||||
active={!finishedLoadingMore && !loading}
|
||||
text="Loading more people"
|
||||
text={t('general.loading.paginate.faces', 'Loading more people')}
|
||||
/>
|
||||
</Layout>
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ import { MY_FACES_QUERY } from '../PeoplePage'
|
|||
import SelectFaceGroupTable from './SelectFaceGroupTable'
|
||||
|
||||
const COMBINE_FACES_MUTATION = gql`
|
||||
mutation($destID: ID!, $srcID: ID!) {
|
||||
mutation combineFaces($destID: ID!, $srcID: ID!) {
|
||||
combineFaceGroups(
|
||||
destinationFaceGroupID: $destID
|
||||
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 Layout from '../../Layout'
|
||||
import PropTypes from 'prop-types'
|
||||
import TimelineGallery from '../../components/timelineGallery/TimelineGallery'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const PhotosPage = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout title="Photos">
|
||||
<Layout title={t('photos_page.title', 'Photos')}>
|
||||
<TimelineGallery />
|
||||
</Layout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
PhotosPage.propTypes = {
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
subPage: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
|
||||
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[];
|
||||
}
|
|
@ -3,6 +3,12 @@ import React, { useRef, useState } from 'react'
|
|||
import { useMutation, useQuery } from '@apollo/client'
|
||||
import { Checkbox, Dropdown, Input, Loader } from 'semantic-ui-react'
|
||||
import { InputLabelDescription, InputLabelTitle } from './SettingsPage'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { scanIntervalQuery } from './__generated__/scanIntervalQuery'
|
||||
import {
|
||||
changeScanIntervalMutation,
|
||||
changeScanIntervalMutationVariables,
|
||||
} from './__generated__/changeScanIntervalMutation'
|
||||
|
||||
const SCAN_INTERVAL_QUERY = gql`
|
||||
query scanIntervalQuery {
|
||||
|
@ -18,44 +24,57 @@ const SCAN_INTERVAL_MUTATION = gql`
|
|||
}
|
||||
`
|
||||
|
||||
enum TimeUnit {
|
||||
Second = 'second',
|
||||
Minute = 'minute',
|
||||
Hour = 'hour',
|
||||
Day = 'day',
|
||||
Month = 'month',
|
||||
}
|
||||
|
||||
const timeUnits = [
|
||||
{
|
||||
value: 'second',
|
||||
value: TimeUnit.Second,
|
||||
multiplier: 1,
|
||||
},
|
||||
{
|
||||
value: 'minute',
|
||||
value: TimeUnit.Minute,
|
||||
multiplier: 60,
|
||||
},
|
||||
{
|
||||
value: 'hour',
|
||||
value: TimeUnit.Hour,
|
||||
multiplier: 60 * 60,
|
||||
},
|
||||
{
|
||||
value: 'day',
|
||||
value: TimeUnit.Day,
|
||||
multiplier: 60 * 60 * 24,
|
||||
},
|
||||
{
|
||||
value: 'month',
|
||||
value: TimeUnit.Month,
|
||||
multiplier: 60 * 60 * 24 * 30,
|
||||
},
|
||||
]
|
||||
|
||||
const convertToSeconds = ({ value, unit }) => {
|
||||
return parseInt(value * timeUnits.find(x => x.value == unit).multiplier)
|
||||
type TimeValue = {
|
||||
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) {
|
||||
return {
|
||||
unit: 'second',
|
||||
unit: TimeUnit.Second,
|
||||
value: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const seconds = convertToSeconds({ value, unit })
|
||||
|
||||
let resultingUnit = timeUnits.first
|
||||
let resultingUnit = timeUnits[0]
|
||||
for (const unit of timeUnits) {
|
||||
if (
|
||||
seconds / unit.multiplier >= 1 &&
|
||||
|
@ -74,27 +93,29 @@ const convertToAppropriateUnit = ({ value, unit }) => {
|
|||
}
|
||||
|
||||
const PeriodicScanner = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [enablePeriodicScanner, setEnablePeriodicScanner] = useState(false)
|
||||
const [scanInterval, setScanInterval] = useState({
|
||||
const [scanInterval, setScanInterval] = useState<TimeValue>({
|
||||
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) {
|
||||
const queryScanInterval = data.siteInfo.periodicScanInterval
|
||||
|
||||
if (queryScanInterval == 0) {
|
||||
setScanInterval({
|
||||
unit: 'second',
|
||||
value: '',
|
||||
unit: TimeUnit.Second,
|
||||
value: 0,
|
||||
})
|
||||
} else {
|
||||
setScanInterval(
|
||||
convertToAppropriateUnit({
|
||||
unit: 'second',
|
||||
unit: TimeUnit.Second,
|
||||
value: queryScanInterval,
|
||||
})
|
||||
)
|
||||
|
@ -107,15 +128,20 @@ const PeriodicScanner = () => {
|
|||
const [
|
||||
setScanIntervalMutation,
|
||||
{ loading: scanIntervalMutationLoading },
|
||||
] = useMutation(SCAN_INTERVAL_MUTATION)
|
||||
] = useMutation<
|
||||
changeScanIntervalMutation,
|
||||
changeScanIntervalMutationVariables
|
||||
>(SCAN_INTERVAL_MUTATION)
|
||||
|
||||
const onScanIntervalCheckboxChange = checked => {
|
||||
const onScanIntervalCheckboxChange = (checked: boolean) => {
|
||||
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)
|
||||
|
||||
if (scanIntervalServerValue.current != seconds) {
|
||||
|
@ -128,62 +154,81 @@ const PeriodicScanner = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const scanIntervalUnits = [
|
||||
type scanIntervalUnitType = {
|
||||
key: TimeUnit
|
||||
text: string
|
||||
value: TimeUnit
|
||||
}
|
||||
|
||||
const scanIntervalUnits: scanIntervalUnitType[] = [
|
||||
{
|
||||
key: 'second',
|
||||
text: 'Seconds',
|
||||
value: 'second',
|
||||
key: TimeUnit.Second,
|
||||
text: t('settings.periodic_scanner.interval_unit.seconds', 'Seconds'),
|
||||
value: TimeUnit.Second,
|
||||
},
|
||||
{
|
||||
key: 'minute',
|
||||
text: 'Minutes',
|
||||
value: 'minute',
|
||||
key: TimeUnit.Minute,
|
||||
text: t('settings.periodic_scanner.interval_unit.minutes', 'Minutes'),
|
||||
value: TimeUnit.Minute,
|
||||
},
|
||||
{
|
||||
key: 'hour',
|
||||
text: 'Hours',
|
||||
value: 'hour',
|
||||
key: TimeUnit.Hour,
|
||||
text: t('settings.periodic_scanner.interval_unit.hour', 'Hour'),
|
||||
value: TimeUnit.Hour,
|
||||
},
|
||||
{
|
||||
key: 'day',
|
||||
text: 'Days',
|
||||
value: 'day',
|
||||
key: TimeUnit.Day,
|
||||
text: t('settings.periodic_scanner.interval_unit.days', 'Days'),
|
||||
value: TimeUnit.Day,
|
||||
},
|
||||
{
|
||||
key: 'month',
|
||||
text: 'Months',
|
||||
value: 'month',
|
||||
key: TimeUnit.Month,
|
||||
text: t('settings.periodic_scanner.interval_unit.months', 'Months'),
|
||||
value: TimeUnit.Month,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>Periodic scanner</h3>
|
||||
<h3>{t('settings.periodic_scanner.title', 'Periodic scanner')}</h3>
|
||||
|
||||
<div style={{ margin: '12px 0' }}>
|
||||
<Checkbox
|
||||
label="Enable periodic scanner"
|
||||
label={t(
|
||||
'settings.periodic_scanner.checkbox_label',
|
||||
'Enable periodic scanner'
|
||||
)}
|
||||
disabled={scanIntervalQuery.loading}
|
||||
checked={enablePeriodicScanner}
|
||||
onChange={(_, { checked }) => onScanIntervalCheckboxChange(checked)}
|
||||
onChange={(_, { checked }) =>
|
||||
onScanIntervalCheckboxChange(checked || false)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{enablePeriodicScanner && (
|
||||
<>
|
||||
<label htmlFor="periodic_scan_field">
|
||||
<InputLabelTitle>Periodic scan interval</InputLabelTitle>
|
||||
<InputLabelTitle>
|
||||
{t(
|
||||
'settings.periodic_scanner.field.label',
|
||||
'Periodic scan interval'
|
||||
)}
|
||||
</InputLabelTitle>
|
||||
<InputLabelDescription>
|
||||
How often the scanner should perform automatic scans of all users
|
||||
{t(
|
||||
'settings.periodic_scanner.field.description',
|
||||
'How often the scanner should perform automatic scans of all users'
|
||||
)}
|
||||
</InputLabelDescription>
|
||||
</label>
|
||||
<Input
|
||||
label={
|
||||
<Dropdown
|
||||
onChange={(_, { value }) => {
|
||||
const newScanInterval = {
|
||||
const newScanInterval: TimeValue = {
|
||||
...scanInterval,
|
||||
unit: value,
|
||||
unit: value as TimeUnit,
|
||||
}
|
||||
|
||||
setScanInterval(newScanInterval)
|
||||
|
@ -194,7 +239,7 @@ const PeriodicScanner = () => {
|
|||
/>
|
||||
}
|
||||
onBlur={() => onScanIntervalUpdate(scanInterval)}
|
||||
onKeyDown={({ key }) =>
|
||||
onKeyDown={({ key }: KeyboardEvent) =>
|
||||
key == 'Enter' && onScanIntervalUpdate(scanInterval)
|
||||
}
|
||||
loading={scanIntervalQuery.loading}
|
||||
|
@ -205,7 +250,7 @@ const PeriodicScanner = () => {
|
|||
onChange={(_, { value }) => {
|
||||
setScanInterval(x => ({
|
||||
...x,
|
||||
value,
|
||||
value: parseInt(value),
|
||||
}))
|
||||
}}
|
||||
/>
|
|
@ -2,6 +2,12 @@ import React, { useRef, useState } from 'react'
|
|||
import { useQuery, useMutation, gql } from '@apollo/client'
|
||||
import { Input, Loader } from 'semantic-ui-react'
|
||||
import { InputLabelTitle, InputLabelDescription } from './SettingsPage'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { concurrentWorkersQuery } from './__generated__/concurrentWorkersQuery'
|
||||
import {
|
||||
setConcurrentWorkers,
|
||||
setConcurrentWorkersVariables,
|
||||
} from './__generated__/setConcurrentWorkers'
|
||||
|
||||
const CONCURRENT_WORKERS_QUERY = gql`
|
||||
query concurrentWorkersQuery {
|
||||
|
@ -18,21 +24,27 @@ const SET_CONCURRENT_WORKERS_MUTATION = gql`
|
|||
`
|
||||
|
||||
const ScannerConcurrentWorkers = () => {
|
||||
const workerAmountQuery = useQuery(CONCURRENT_WORKERS_QUERY, {
|
||||
onCompleted(data) {
|
||||
setWorkerAmount(data.siteInfo.concurrentWorkers)
|
||||
workerAmountServerValue.current = data.siteInfo.concurrentWorkers
|
||||
},
|
||||
})
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [setWorkersMutation, workersMutationData] = useMutation(
|
||||
SET_CONCURRENT_WORKERS_MUTATION
|
||||
const workerAmountQuery = useQuery<concurrentWorkersQuery>(
|
||||
CONCURRENT_WORKERS_QUERY,
|
||||
{
|
||||
onCompleted(data) {
|
||||
setWorkerAmount(data.siteInfo.concurrentWorkers)
|
||||
workerAmountServerValue.current = data.siteInfo.concurrentWorkers
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const workerAmountServerValue = useRef(null)
|
||||
const [workerAmount, setWorkerAmount] = useState('')
|
||||
const [setWorkersMutation, workersMutationData] = useMutation<
|
||||
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) {
|
||||
workerAmountServerValue.current = workerAmount
|
||||
setWorkersMutation({
|
||||
|
@ -46,9 +58,14 @@ const ScannerConcurrentWorkers = () => {
|
|||
return (
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<label htmlFor="scanner_concurrent_workers_field">
|
||||
<InputLabelTitle>Scanner Concurrent Workers</InputLabelTitle>
|
||||
<InputLabelTitle>
|
||||
{t('settings.concurrent_workers.title', 'Scanner concurrent workers')}
|
||||
</InputLabelTitle>
|
||||
<InputLabelDescription>
|
||||
The maximum amount of scanner jobs that is allowed to run at once
|
||||
{t(
|
||||
'settings.concurrent_workers.description',
|
||||
'The maximum amount of scanner jobs that is allowed to run at once'
|
||||
)}
|
||||
</InputLabelDescription>
|
||||
</label>
|
||||
<Input
|
||||
|
@ -59,10 +76,10 @@ const ScannerConcurrentWorkers = () => {
|
|||
id="scanner_concurrent_workers_field"
|
||||
value={workerAmount}
|
||||
onChange={(_, { value }) => {
|
||||
setWorkerAmount(value)
|
||||
setWorkerAmount(parseInt(value))
|
||||
}}
|
||||
onBlur={() => updateWorkerAmount(workerAmount)}
|
||||
onKeyDown={({ key }) =>
|
||||
onKeyDown={({ key }: KeyboardEvent) =>
|
||||
key == 'Enter' && updateWorkerAmount(workerAmount)
|
||||
}
|
||||
/>
|
|
@ -4,6 +4,8 @@ import { Button, Icon } from 'semantic-ui-react'
|
|||
import PeriodicScanner from './PeriodicScanner'
|
||||
import ScannerConcurrentWorkers from './ScannerConcurrentWorkers'
|
||||
import { SectionTitle, InputLabelDescription } from './SettingsPage'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { scanAllMutation } from './__generated__/scanAllMutation'
|
||||
|
||||
const SCAN_MUTATION = gql`
|
||||
mutation scanAllMutation {
|
||||
|
@ -15,13 +17,19 @@ const SCAN_MUTATION = gql`
|
|||
`
|
||||
|
||||
const ScannerSection = () => {
|
||||
const [startScanner, { called }] = useMutation(SCAN_MUTATION)
|
||||
const { t } = useTranslation()
|
||||
const [startScanner, { called }] = useMutation<scanAllMutation>(SCAN_MUTATION)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SectionTitle nospace>Scanner</SectionTitle>
|
||||
<SectionTitle nospace>
|
||||
{t('settings.scanner.title', 'Scanner')}
|
||||
</SectionTitle>
|
||||
<InputLabelDescription>
|
||||
Will scan all users for new or updated media
|
||||
{t(
|
||||
'settings.scanner.description',
|
||||
'Will scan all users for new or updated media'
|
||||
)}
|
||||
</InputLabelDescription>
|
||||
<Button
|
||||
icon
|
||||
|
@ -32,7 +40,7 @@ const ScannerSection = () => {
|
|||
disabled={called}
|
||||
>
|
||||
<Icon name="sync" />
|
||||
Scan all users
|
||||
{t('settings.scanner.scan_all_users', 'Scan all users')}
|
||||
</Button>
|
||||
<PeriodicScanner />
|
||||
<ScannerConcurrentWorkers />
|
|
@ -1,35 +0,0 @@
|
|||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Layout from '../../Layout'
|
||||
|
||||
import ScannerSection from './ScannerSection'
|
||||
import UsersTable from './Users/UsersTable'
|
||||
|
||||
export const SectionTitle = styled.h2`
|
||||
margin-top: ${({ nospace }) => (nospace ? '0' : '1.4em')} !important;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid #ddd;
|
||||
`
|
||||
|
||||
export const InputLabelTitle = styled.p`
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
margin: 1em 0 0 !important;
|
||||
`
|
||||
|
||||
export const InputLabelDescription = styled.p`
|
||||
font-size: 0.9em;
|
||||
margin: 0 0 0.5em !important;
|
||||
`
|
||||
|
||||
const SettingsPage = () => {
|
||||
return (
|
||||
<Layout title="Settings">
|
||||
<ScannerSection />
|
||||
<UsersTable />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsPage
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from 'semantic-ui-react'
|
||||
import styled from 'styled-components'
|
||||
import { useIsAdmin } from '../../components/routes/AuthorizedRoute'
|
||||
|
||||
import Layout from '../../Layout'
|
||||
|
||||
import ScannerSection from './ScannerSection'
|
||||
import UserPreferences from './UserPreferences'
|
||||
import UsersTable from './Users/UsersTable'
|
||||
|
||||
export const SectionTitle = styled.h2<{ nospace?: boolean }>`
|
||||
margin-top: ${({ nospace }) => (nospace ? '0' : '1.4em')} !important;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid #ddd;
|
||||
`
|
||||
|
||||
export const InputLabelTitle = styled.p`
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
margin: 1em 0 0 !important;
|
||||
`
|
||||
|
||||
export const InputLabelDescription = styled.p`
|
||||
font-size: 0.9em;
|
||||
margin: 0 0 0.5em !important;
|
||||
`
|
||||
|
||||
const SettingsPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const isAdmin = useIsAdmin()
|
||||
|
||||
return (
|
||||
<Layout title={t('title.settings', 'Settings')}>
|
||||
<UserPreferences />
|
||||
{isAdmin && (
|
||||
<>
|
||||
<ScannerSection />
|
||||
<UsersTable />
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
style={{ marginTop: 24 }}
|
||||
onClick={() => {
|
||||
location.href = '/logout'
|
||||
}}
|
||||
>
|
||||
{t('settings.logout', 'Log out')}
|
||||
</Button>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsPage
|
|
@ -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,9 +1,9 @@
|
|||
import { gql, useMutation } from '@apollo/client'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Checkbox, Input, Table } from 'semantic-ui-react'
|
||||
|
||||
const createUserMutation = gql`
|
||||
const CREATE_USER_MUTATION = gql`
|
||||
mutation createUser($username: String!, $admin: Boolean!) {
|
||||
createUser(username: $username, admin: $admin) {
|
||||
id
|
||||
|
@ -14,7 +14,7 @@ const createUserMutation = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const addRootPathMutation = gql`
|
||||
export const USER_ADD_ROOT_PATH_MUTATION = gql`
|
||||
mutation userAddRootPath($id: ID!, $rootPath: String!) {
|
||||
userAddRootPath(id: $id, rootPath: $rootPath) {
|
||||
id
|
||||
|
@ -29,11 +29,18 @@ const initialState = {
|
|||
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 [state, setState] = useState(initialState)
|
||||
|
||||
const [addRootPath, { loading: addRootPathLoading }] = useMutation(
|
||||
addRootPathMutation,
|
||||
USER_ADD_ROOT_PATH_MUTATION,
|
||||
{
|
||||
onCompleted: () => {
|
||||
setState(initialState)
|
||||
|
@ -47,7 +54,7 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => {
|
|||
)
|
||||
|
||||
const [createUser, { loading: createUserLoading }] = useMutation(
|
||||
createUserMutation,
|
||||
CREATE_USER_MUTATION,
|
||||
{
|
||||
onCompleted: ({ createUser: { id } }) => {
|
||||
if (state.rootPath) {
|
||||
|
@ -66,7 +73,10 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => {
|
|||
|
||||
const loading = addRootPathLoading || createUserLoading
|
||||
|
||||
function updateInput(event, key) {
|
||||
function updateInput(
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
key: string
|
||||
) {
|
||||
setState({
|
||||
...state,
|
||||
[key]: event.target.value,
|
||||
|
@ -81,14 +91,17 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => {
|
|||
<Table.Row>
|
||||
<Table.Cell>
|
||||
<Input
|
||||
placeholder="Username"
|
||||
placeholder={t('login_page.field.username', 'Username')}
|
||||
value={state.username}
|
||||
onChange={e => updateInput(e, 'username')}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Input
|
||||
placeholder="/path/to/photos"
|
||||
placeholder={t(
|
||||
'login_page.initial_setup.field.photo_path.placeholder',
|
||||
'/path/to/photos'
|
||||
)}
|
||||
value={state.rootPath}
|
||||
onChange={e => updateInput(e, 'rootPath')}
|
||||
/>
|
||||
|
@ -100,7 +113,7 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => {
|
|||
onChange={(e, data) => {
|
||||
setState({
|
||||
...state,
|
||||
admin: data.checked,
|
||||
admin: data.checked || false,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
@ -108,7 +121,7 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => {
|
|||
<Table.Cell>
|
||||
<Button.Group>
|
||||
<Button negative onClick={() => setShow(false)}>
|
||||
Cancel
|
||||
{t('general.action.cancel', 'Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
|
@ -124,7 +137,7 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => {
|
|||
})
|
||||
}}
|
||||
>
|
||||
Add User
|
||||
{t('settings.users.add_user.submit', 'Add user')}
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</Table.Cell>
|
||||
|
@ -132,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
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Checkbox, Input, Table } from 'semantic-ui-react'
|
||||
import { EditRootPaths } from './EditUserRowRootPaths'
|
||||
import { UserRowProps } from './UserRow'
|
||||
import { UserRowProps, UserRowChildProps } from './UserRow'
|
||||
|
||||
const EditUserRow = ({
|
||||
user,
|
||||
|
@ -9,8 +10,13 @@ const EditUserRow = ({
|
|||
setState,
|
||||
updateUser,
|
||||
updateUserLoading,
|
||||
}) => {
|
||||
function updateInput(event, key) {
|
||||
}: UserRowChildProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
function updateInput(
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
key: string
|
||||
) {
|
||||
setState(state => ({
|
||||
...state,
|
||||
[key]: event.target.value,
|
||||
|
@ -37,7 +43,7 @@ const EditUserRow = ({
|
|||
onChange={(_, data) => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
admin: data.checked,
|
||||
admin: data.checked || false,
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
|
@ -48,11 +54,12 @@ const EditUserRow = ({
|
|||
negative
|
||||
onClick={() =>
|
||||
setState(state => ({
|
||||
...state,
|
||||
...state.oldState,
|
||||
}))
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
{t('general.action.cancel', 'Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
loading={updateUserLoading}
|
||||
|
@ -68,7 +75,7 @@ const EditUserRow = ({
|
|||
})
|
||||
}
|
||||
>
|
||||
Save
|
||||
{t('general.action.save', 'Save')}
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</Table.Cell>
|
|
@ -1,19 +1,21 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React, { useState } from 'react'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import { Button, Icon, Input } from 'semantic-ui-react'
|
||||
import styled from 'styled-components'
|
||||
import { USERS_QUERY } from './UsersTable'
|
||||
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`
|
||||
mutation userAddRootPath($id: ID!, $rootPath: String!) {
|
||||
userAddRootPath(id: $id, rootPath: $rootPath) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const userRemoveAlbumPathMutation = gql`
|
||||
const USER_REMOVE_ALBUM_PATH_MUTATION = gql`
|
||||
mutation userRemoveAlbumPathMutation($userId: ID!, $albumId: ID!) {
|
||||
userRemoveRootAlbum(userId: $userId, albumId: $albumId) {
|
||||
id
|
||||
|
@ -27,17 +29,23 @@ const RootPathListItem = styled.li`
|
|||
align-items: center;
|
||||
`
|
||||
|
||||
const EditRootPath = ({ album, user }) => {
|
||||
const [removeAlbumPath, { loading }] = useMutation(
|
||||
type EditRootPathProps = {
|
||||
album: settingsUsersQuery_user_rootAlbums
|
||||
user: settingsUsersQuery_user
|
||||
}
|
||||
|
||||
const EditRootPath = ({ album, user }: EditRootPathProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [removeAlbumPath, { loading }] = useMutation<
|
||||
userRemoveAlbumPathMutation,
|
||||
{
|
||||
refetchQueries: [
|
||||
{
|
||||
query: USERS_QUERY,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
userRemoveAlbumPathMutationVariables
|
||||
>(USER_REMOVE_ALBUM_PATH_MUTATION, {
|
||||
refetchQueries: [
|
||||
{
|
||||
query: USERS_QUERY,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
return (
|
||||
<RootPathListItem>
|
||||
|
@ -55,43 +63,48 @@ const EditRootPath = ({ album, user }) => {
|
|||
}
|
||||
>
|
||||
<Icon name="remove" />
|
||||
Remove
|
||||
{t('general.action.remove', 'Remove')}
|
||||
</Button>
|
||||
</RootPathListItem>
|
||||
)
|
||||
}
|
||||
|
||||
EditRootPath.propTypes = {
|
||||
album: PropTypes.object.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
const NewRootPathInput = styled(Input)`
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
`
|
||||
|
||||
const EditNewRootPath = ({ userID }) => {
|
||||
type EditNewRootPathProps = {
|
||||
userID: string
|
||||
}
|
||||
|
||||
const EditNewRootPath = ({ userID }: EditNewRootPathProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [value, setValue] = useState('')
|
||||
const [addRootPath, { loading }] = useMutation(userAddRootPathMutation, {
|
||||
refetchQueries: [
|
||||
{
|
||||
query: USERS_QUERY,
|
||||
},
|
||||
],
|
||||
})
|
||||
const [addRootPath, { loading }] = useMutation<userAddRootPath>(
|
||||
USER_ADD_ROOT_PATH_MUTATION,
|
||||
{
|
||||
refetchQueries: [
|
||||
{
|
||||
query: USERS_QUERY,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<li>
|
||||
<NewRootPathInput
|
||||
style={{ width: '100%' }}
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setValue(e.target.value)
|
||||
}
|
||||
disabled={loading}
|
||||
action={{
|
||||
positive: true,
|
||||
icon: 'add',
|
||||
content: 'Add',
|
||||
content: t('general.action.add', 'Add'),
|
||||
onClick: () => {
|
||||
setValue('')
|
||||
addRootPath({
|
||||
|
@ -107,17 +120,17 @@ const EditNewRootPath = ({ userID }) => {
|
|||
)
|
||||
}
|
||||
|
||||
EditNewRootPath.propTypes = {
|
||||
userID: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
const RootPathList = styled.ul`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
`
|
||||
|
||||
export const EditRootPaths = ({ user }) => {
|
||||
type EditRootPathsProps = {
|
||||
user: settingsUsersQuery_user
|
||||
}
|
||||
|
||||
export const EditRootPaths = ({ user }: EditRootPathsProps) => {
|
||||
const editRows = user.rootAlbums.map(album => (
|
||||
<EditRootPath key={album.id} album={album} user={user} />
|
||||
))
|
||||
|
@ -129,7 +142,3 @@ export const EditRootPaths = ({ user }) => {
|
|||
</RootPathList>
|
||||
)
|
||||
}
|
||||
|
||||
EditRootPaths.propTypes = {
|
||||
user: PropTypes.object.isRequired,
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import { Button, Form, Input, Modal } from 'semantic-ui-react'
|
||||
|
||||
const changeUserPasswordMutation = gql`
|
||||
mutation changeUserPassword($userId: ID!, $password: String!) {
|
||||
updateUser(id: $userId, password: $password) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const ChangePasswordModal = ({ onClose, user, ...props }) => {
|
||||
const [passwordInput, setPasswordInput] = useState('')
|
||||
|
||||
const [changePassword] = useMutation(changeUserPasswordMutation, {
|
||||
onCompleted: () => {
|
||||
onClose && onClose()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<Modal.Header>Change password</Modal.Header>
|
||||
<Modal.Content>
|
||||
<p>
|
||||
Change password for <b>{user.username}</b>
|
||||
</p>
|
||||
<Form>
|
||||
<Form.Field>
|
||||
<label>New password</label>
|
||||
<Input
|
||||
placeholder="password"
|
||||
onChange={e => setPasswordInput(e.target.value)}
|
||||
type="password"
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={() => onClose && onClose()}>Cancel</Button>
|
||||
<Button
|
||||
positive
|
||||
onClick={() => {
|
||||
changePassword({
|
||||
variables: {
|
||||
userId: user.id,
|
||||
password: passwordInput,
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
Change password
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
ChangePasswordModal.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
user: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export default ChangePasswordModal
|
|
@ -0,0 +1,85 @@
|
|||
import React, { useState } from 'react'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import { Button, Form, Input, Modal, ModalProps } from 'semantic-ui-react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { settingsUsersQuery_user } from './__generated__/settingsUsersQuery'
|
||||
|
||||
const changeUserPasswordMutation = gql`
|
||||
mutation changeUserPassword($userId: ID!, $password: String!) {
|
||||
updateUser(id: $userId, password: $password) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
interface ChangePasswordModalProps extends ModalProps {
|
||||
onClose(): void
|
||||
open: boolean
|
||||
user: settingsUsersQuery_user
|
||||
}
|
||||
|
||||
const ChangePasswordModal = ({
|
||||
onClose,
|
||||
user,
|
||||
open,
|
||||
...props
|
||||
}: ChangePasswordModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [passwordInput, setPasswordInput] = useState('')
|
||||
|
||||
const [changePassword] = useMutation(changeUserPasswordMutation, {
|
||||
onCompleted: () => {
|
||||
onClose && onClose()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal open={open} {...props}>
|
||||
<Modal.Header>
|
||||
{t('settings.users.password_reset.title', 'Change password')}
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<p>
|
||||
<Trans t={t} i18nKey="settings.users.password_reset.description">
|
||||
Change password for <b>{user.username}</b>
|
||||
</Trans>
|
||||
</p>
|
||||
<Form>
|
||||
<Form.Field>
|
||||
<label>
|
||||
{t('settings.users.password_reset.form.label', 'New password')}
|
||||
</label>
|
||||
<Input
|
||||
placeholder={t(
|
||||
'settings.users.password_reset.form.placeholder',
|
||||
'password'
|
||||
)}
|
||||
onChange={e => setPasswordInput(e.target.value)}
|
||||
type="password"
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={() => onClose && onClose()}>
|
||||
{t('general.action.cancel', 'Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
positive
|
||||
onClick={() => {
|
||||
changePassword({
|
||||
variables: {
|
||||
userId: user.id,
|
||||
password: passwordInput,
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
{t('settings.users.password_reset.form.submit', 'Change password')}
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
|
@ -5,13 +5,14 @@ import { useQuery, gql } from '@apollo/client'
|
|||
import UserRow from './UserRow'
|
||||
import AddUserRow from './AddUserRow'
|
||||
import { SectionTitle } from '../SettingsPage'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { settingsUsersQuery } from './__generated__/settingsUsersQuery'
|
||||
|
||||
export const USERS_QUERY = gql`
|
||||
query settingsUsersQuery {
|
||||
user {
|
||||
id
|
||||
username
|
||||
# rootPath
|
||||
admin
|
||||
rootAlbums {
|
||||
id
|
||||
|
@ -22,16 +23,19 @@ export const USERS_QUERY = gql`
|
|||
`
|
||||
|
||||
const UsersTable = () => {
|
||||
const { t } = useTranslation()
|
||||
const [showAddUser, setShowAddUser] = useState(false)
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(USERS_QUERY)
|
||||
const { loading, error, data, refetch } = useQuery<settingsUsersQuery>(
|
||||
USERS_QUERY
|
||||
)
|
||||
|
||||
if (error) {
|
||||
return `Users table error: ${error.message}`
|
||||
return <div>{`Users table error: ${error.message}`}</div>
|
||||
}
|
||||
|
||||
let userRows = []
|
||||
if (data && data.user) {
|
||||
let userRows: JSX.Element[] = []
|
||||
if (data?.user) {
|
||||
userRows = data.user.map(user => (
|
||||
<UserRow user={user} refetchUsers={refetch} key={user.id} />
|
||||
))
|
||||
|
@ -39,15 +43,23 @@ const UsersTable = () => {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<SectionTitle>Users</SectionTitle>
|
||||
<SectionTitle>{t('settings.users.title', 'Users')}</SectionTitle>
|
||||
<Loader active={loading} />
|
||||
<Table celled>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Username</Table.HeaderCell>
|
||||
<Table.HeaderCell>Photo path</Table.HeaderCell>
|
||||
<Table.HeaderCell>Admin</Table.HeaderCell>
|
||||
<Table.HeaderCell>Action</Table.HeaderCell>
|
||||
<Table.HeaderCell>
|
||||
{t('settings.users.table.column_names.username', 'Username')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell>
|
||||
{t('settings.users.table.column_names.photo_path', 'Photo path')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell>
|
||||
{t('settings.users.table.column_names.admin', 'Admin')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell>
|
||||
{t('settings.users.table.column_names.action', 'Action')}
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
|
@ -73,7 +85,7 @@ const UsersTable = () => {
|
|||
onClick={() => setShowAddUser(true)}
|
||||
>
|
||||
<Icon name="add" />
|
||||
New user
|
||||
{t('settings.users.table.new_user', 'New user')}
|
||||
</Button>
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Button, Icon, Table, Modal } from 'semantic-ui-react'
|
||||
import styled from 'styled-components'
|
||||
import ChangePasswordModal from './UserChangePassword'
|
||||
import { UserRowProps } from './UserRow'
|
||||
import { UserRowChildProps, UserRowProps } from './UserRow'
|
||||
|
||||
const PathList = styled.ul`
|
||||
margin: 0;
|
||||
|
@ -21,7 +22,8 @@ const ViewUserRow = ({
|
|||
scanUserCalled,
|
||||
showChangePassword,
|
||||
showConfirmDelete,
|
||||
}) => {
|
||||
}: UserRowChildProps) => {
|
||||
const { t } = useTranslation()
|
||||
const paths = (
|
||||
<PathList>
|
||||
{user.rootAlbums.map(album => (
|
||||
|
@ -41,22 +43,29 @@ const ViewUserRow = ({
|
|||
<Button.Group>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setState(state => ({ ...state, editing: true, oldState: state }))
|
||||
setState(state => {
|
||||
const oldState = { ...state }
|
||||
delete oldState.oldState
|
||||
return { ...state, editing: true, oldState }
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Icon name="edit" />
|
||||
Edit
|
||||
{t('settings.users.table.row.action.edit', 'Edit')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={scanUserCalled}
|
||||
onClick={() => scanUser({ variables: { userId: user.id } })}
|
||||
>
|
||||
<Icon name="sync" />
|
||||
Scan
|
||||
{t('settings.users.table.row.action.scan', 'Scan')}
|
||||
</Button>
|
||||
<Button onClick={() => setChangePassword(true)}>
|
||||
<Icon name="key" />
|
||||
Change password
|
||||
{t(
|
||||
'settings.users.table.row.action.change_password',
|
||||
'Change password'
|
||||
)}
|
||||
</Button>
|
||||
<ChangePasswordModal
|
||||
user={user}
|
||||
|
@ -70,19 +79,28 @@ const ViewUserRow = ({
|
|||
}}
|
||||
>
|
||||
<Icon name="delete" />
|
||||
Delete
|
||||
{t('settings.users.table.row.action.delete', 'Delete')}
|
||||
</Button>
|
||||
<Modal open={showConfirmDelete}>
|
||||
<Modal.Header>Delete user</Modal.Header>
|
||||
<Modal.Header>
|
||||
{t('settings.users.confirm_delete_user.title', 'Delete user')}
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<p>
|
||||
{`Are you sure, you want to delete `}
|
||||
<b>{user.username}</b>?
|
||||
</p>
|
||||
<p>{`This action cannot be undone`}</p>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="settings.users.confirm_delete_user.description"
|
||||
>
|
||||
<p>
|
||||
{`Are you sure, you want to delete `}
|
||||
<b>{user.username}</b>?
|
||||
</p>
|
||||
<p>{`This action cannot be undone`}</p>
|
||||
</Trans>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={() => setConfirmDelete(false)}>Cancel</Button>
|
||||
<Button onClick={() => setConfirmDelete(false)}>
|
||||
{t('general.action.cancel', 'Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
negative
|
||||
onClick={() => {
|
||||
|
@ -94,7 +112,11 @@ const ViewUserRow = ({
|
|||
})
|
||||
}}
|
||||
>
|
||||
Delete {user.username}
|
||||
{t(
|
||||
'settings.users.confirm_delete_user.action',
|
||||
'Delete {{user}}',
|
||||
{ user: user.username }
|
||||
)}
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
|
@ -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,9 +1,9 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import Layout from '../../Layout'
|
||||
import AlbumGallery from '../../components/albumGallery/AlbumGallery'
|
||||
import styled from 'styled-components'
|
||||
import { gql, useQuery } from '@apollo/client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const SHARE_ALBUM_QUERY = gql`
|
||||
query shareAlbumQuery(
|
||||
|
@ -72,7 +72,14 @@ const AlbumSharePageWrapper = styled.div`
|
|||
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 { data, loading, error } = useQuery(SHARE_ALBUM_QUERY, {
|
||||
variables: {
|
||||
id: albumID,
|
||||
|
@ -84,18 +91,22 @@ const AlbumSharePage = ({ albumID, token, password }) => {
|
|||
})
|
||||
|
||||
if (error) {
|
||||
return error.message
|
||||
return <div>{error.message}</div>
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return 'Loading...'
|
||||
return <div>{t('general.loading.default', 'Loading...')}</div>
|
||||
}
|
||||
|
||||
const album = data.album
|
||||
|
||||
return (
|
||||
<AlbumSharePageWrapper data-testid="AlbumSharePage">
|
||||
<Layout title={album ? album.title : 'Loading album'}>
|
||||
<Layout
|
||||
title={
|
||||
album ? album.title : t('general.loading.album', 'Loading album')
|
||||
}
|
||||
>
|
||||
<AlbumGallery
|
||||
album={album}
|
||||
customAlbumLink={albumId => `/share/${token}/${albumId}`}
|
||||
|
@ -105,10 +116,4 @@ const AlbumSharePage = ({ albumID, token, password }) => {
|
|||
)
|
||||
}
|
||||
|
||||
AlbumSharePage.propTypes = {
|
||||
albumID: PropTypes.string.isRequired,
|
||||
token: PropTypes.string.isRequired,
|
||||
password: PropTypes.string,
|
||||
}
|
||||
|
||||
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
|
|
@ -18,6 +18,8 @@ import SharePage, {
|
|||
import { SIDEBAR_DOWNLOAD_QUERY } from '../../components/sidebar/SidebarDownload'
|
||||
import { SHARE_ALBUM_QUERY } from './AlbumSharePage'
|
||||
|
||||
require('../../localization').default()
|
||||
|
||||
describe('load correct share page, based on graphql query', () => {
|
||||
const token = 'TOKEN123'
|
||||
|
||||
|
@ -80,7 +82,7 @@ describe('load correct share page, based on graphql query', () => {
|
|||
media: {
|
||||
id: '1',
|
||||
title: 'shared_image.jpg',
|
||||
type: 'photo',
|
||||
type: 'Photo',
|
||||
highRes: {
|
||||
url: 'https://example.com/shared_image.jpg',
|
||||
},
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React, { useState } from 'react'
|
||||
import { useQuery, gql } from '@apollo/client'
|
||||
import { Route, Switch } from 'react-router-dom'
|
||||
import RouterProps from 'react-router-prop-types'
|
||||
import { match as MatchType, Route, Switch } from 'react-router-dom'
|
||||
import { Form, Header, Icon, Input, Message } from 'semantic-ui-react'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
|
@ -11,6 +10,7 @@ import {
|
|||
} from '../../helpers/authentication'
|
||||
import AlbumSharePage from './AlbumSharePage'
|
||||
import MediaSharePage from './MediaSharePage'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const SHARE_TOKEN_QUERY = gql`
|
||||
query SharePageToken($token: String!, $password: String) {
|
||||
|
@ -44,8 +44,11 @@ export const SHARE_TOKEN_QUERY = gql`
|
|||
}
|
||||
videoWeb {
|
||||
url
|
||||
width
|
||||
height
|
||||
}
|
||||
exif {
|
||||
id
|
||||
camera
|
||||
maker
|
||||
lens
|
||||
|
@ -70,7 +73,9 @@ export const VALIDATE_TOKEN_PASSWORD_QUERY = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const AuthorizedTokenRoute = ({ match }) => {
|
||||
const AuthorizedTokenRoute = ({ match }: MatchProps<TokenRouteMatch>) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const token = match.params.token
|
||||
const password = getSharePassword(token)
|
||||
|
||||
|
@ -81,12 +86,11 @@ const AuthorizedTokenRoute = ({ match }) => {
|
|||
},
|
||||
})
|
||||
|
||||
if (error) return error.message
|
||||
if (loading) return 'Loading...'
|
||||
if (error) return <div>{error.message}</div>
|
||||
if (loading) return <div>{t('general.loading.default', 'Loading...')}</div>
|
||||
|
||||
if (data.shareToken.album) {
|
||||
const SharedSubAlbumPage = ({ match }) => {
|
||||
console.log('subalbum match', match)
|
||||
const SharedSubAlbumPage = ({ match }: MatchProps<SubalbumRouteMatch>) => {
|
||||
return (
|
||||
<AlbumSharePage
|
||||
albumID={match.params.subAlbum}
|
||||
|
@ -122,7 +126,7 @@ const AuthorizedTokenRoute = ({ match }) => {
|
|||
return <MediaSharePage media={data.shareToken.media} />
|
||||
}
|
||||
|
||||
return <h1>Share not found</h1>
|
||||
return <h1>{t('share_page.share_not_found', 'Share not found')}</h1>
|
||||
}
|
||||
|
||||
AuthorizedTokenRoute.propTypes = {
|
||||
|
@ -134,10 +138,17 @@ const MessageContainer = styled.div`
|
|||
margin: 100px auto 0;
|
||||
`
|
||||
|
||||
type ProtectedTokenEnterPasswordProps = {
|
||||
refetchWithPassword(password: string): void
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const ProtectedTokenEnterPassword = ({
|
||||
refetchWithPassword,
|
||||
loading = false,
|
||||
}) => {
|
||||
}: ProtectedTokenEnterPasswordProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [passwordValue, setPasswordValue] = useState('')
|
||||
const [invalidPassword, setInvalidPassword] = useState(false)
|
||||
|
||||
|
@ -150,7 +161,9 @@ const ProtectedTokenEnterPassword = ({
|
|||
if (invalidPassword && !loading) {
|
||||
errorMessage = (
|
||||
<Message negative>
|
||||
<Message.Content>Wrong password, please try again.</Message.Content>
|
||||
<Message.Content>
|
||||
{t('share_page.wrong_password', 'Wrong password, please try again.')}
|
||||
</Message.Content>
|
||||
</Message>
|
||||
)
|
||||
}
|
||||
|
@ -158,18 +171,25 @@ const ProtectedTokenEnterPassword = ({
|
|||
return (
|
||||
<MessageContainer>
|
||||
<Header as="h1" style={{ fontWeight: 400 }}>
|
||||
Protected share
|
||||
{t('share_page.protected_share.title', 'Protected share')}
|
||||
</Header>
|
||||
<p>This share is protected with a password.</p>
|
||||
<p>
|
||||
{t(
|
||||
'share_page.protected_share.description',
|
||||
'This share is protected with a password.'
|
||||
)}
|
||||
</p>
|
||||
<Form>
|
||||
<Form.Field>
|
||||
<label>Password</label>
|
||||
<label>{t('login_page.field.password', 'Password')}</label>
|
||||
<Input
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
onKeyUp={event => event.key == 'Enter' && onSubmit()}
|
||||
onKeyUp={(event: KeyboardEvent) =>
|
||||
event.key == 'Enter' && onSubmit()
|
||||
}
|
||||
onChange={e => setPasswordValue(e.target.value)}
|
||||
placeholder="Password"
|
||||
placeholder={t('login_page.field.password', 'Password')}
|
||||
type="password"
|
||||
icon={<Icon onClick={onSubmit} link name="arrow right" />}
|
||||
/>
|
||||
|
@ -180,12 +200,21 @@ const ProtectedTokenEnterPassword = ({
|
|||
)
|
||||
}
|
||||
|
||||
ProtectedTokenEnterPassword.propTypes = {
|
||||
refetchWithPassword: PropTypes.func.isRequired,
|
||||
loading: PropTypes.bool,
|
||||
interface TokenRouteMatch {
|
||||
token: string
|
||||
}
|
||||
|
||||
const TokenRoute = ({ match }) => {
|
||||
interface SubalbumRouteMatch extends TokenRouteMatch {
|
||||
subAlbum: string
|
||||
}
|
||||
|
||||
interface MatchProps<Route> {
|
||||
match: MatchType<Route>
|
||||
}
|
||||
|
||||
const TokenRoute = ({ match }: MatchProps<TokenRouteMatch>) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const token = match.params.token
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(
|
||||
|
@ -203,19 +232,23 @@ const TokenRoute = ({ match }) => {
|
|||
if (error.message == 'GraphQL error: share not found') {
|
||||
return (
|
||||
<MessageContainer>
|
||||
<h1>Share not found</h1>
|
||||
<p>Maybe the share has expired or has been deleted.</p>
|
||||
<h1>{t('share_page.share_not_found', 'Share not found')}</h1>
|
||||
<p>
|
||||
{t(
|
||||
'share_page.share_not_found_description',
|
||||
'Maybe the share has expired or has been deleted.'
|
||||
)}
|
||||
</p>
|
||||
</MessageContainer>
|
||||
)
|
||||
}
|
||||
|
||||
return error.message
|
||||
return <div>{error.message}</div>
|
||||
}
|
||||
|
||||
if (data && data.shareTokenValidatePassword == false) {
|
||||
return (
|
||||
<ProtectedTokenEnterPassword
|
||||
match={match}
|
||||
refetchWithPassword={password => {
|
||||
saveSharePassword(token, password)
|
||||
refetch({ token, password })
|
||||
|
@ -225,28 +258,24 @@ const TokenRoute = ({ match }) => {
|
|||
)
|
||||
}
|
||||
|
||||
if (loading) return 'Loading...'
|
||||
if (loading) return <div>{t('general.loading.default', 'Loading...')}</div>
|
||||
|
||||
return <AuthorizedTokenRoute match={match} />
|
||||
}
|
||||
|
||||
TokenRoute.propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
}
|
||||
const SharePage = ({ match }: { match: MatchType }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const SharePage = ({ match }) => (
|
||||
<Switch>
|
||||
<Route path={`${match.url}/:token`}>
|
||||
{({ match }) => {
|
||||
return <TokenRoute match={match} />
|
||||
}}
|
||||
</Route>
|
||||
<Route path="/">Route not found</Route>
|
||||
</Switch>
|
||||
)
|
||||
|
||||
SharePage.propTypes = {
|
||||
...RouterProps,
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${match.url}/:token`}>
|
||||
{({ match }: { match: MatchType<TokenRouteMatch> }) => {
|
||||
return <TokenRoute match={match} />
|
||||
}}
|
||||
</Route>
|
||||
<Route path="/">{t('routes.page_not_found', 'Page not found')}</Route>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
ApolloLink,
|
||||
HttpLink,
|
||||
ServerError,
|
||||
FieldMergeFunction,
|
||||
} from '@apollo/client'
|
||||
import { getMainDefinition } from '@apollo/client/utilities'
|
||||
import { onError } from '@apollo/client/link/error'
|
||||
|
@ -12,6 +14,7 @@ import { WebSocketLink } from '@apollo/client/link/ws'
|
|||
import urlJoin from 'url-join'
|
||||
import { clearTokenCookie } from './helpers/authentication'
|
||||
import { MessageState } from './components/messages/Messages'
|
||||
import { Message } from './components/messages/SubscriptionsHook'
|
||||
|
||||
export const GRAPHQL_ENDPOINT = process.env.PHOTOVIEW_API_ENDPOINT
|
||||
? urlJoin(process.env.PHOTOVIEW_API_ENDPOINT, '/graphql')
|
||||
|
@ -26,12 +29,12 @@ console.log('GRAPHQL ENDPOINT', GRAPHQL_ENDPOINT)
|
|||
|
||||
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:'
|
||||
|
||||
const wsLink = new WebSocketLink({
|
||||
uri: websocketUri,
|
||||
credentials: 'include',
|
||||
uri: websocketUri.toString(),
|
||||
// credentials: 'include',
|
||||
})
|
||||
|
||||
const link = split(
|
||||
|
@ -48,7 +51,7 @@ const link = split(
|
|||
)
|
||||
|
||||
const linkError = onError(({ graphQLErrors, networkError }) => {
|
||||
let errorMessages = []
|
||||
const errorMessages = []
|
||||
|
||||
if (graphQLErrors) {
|
||||
graphQLErrors.map(({ message, locations, path }) =>
|
||||
|
@ -82,7 +85,7 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
|
|||
console.log(`[Network error]: ${JSON.stringify(networkError)}`)
|
||||
clearTokenCookie()
|
||||
|
||||
const errors = networkError.result.errors
|
||||
const errors = (networkError as ServerError).result.errors
|
||||
|
||||
if (errors.length == 1) {
|
||||
errorMessages.push({
|
||||
|
@ -92,7 +95,9 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
|
|||
} else if (errors.length > 1) {
|
||||
errorMessages.push({
|
||||
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,
|
||||
},
|
||||
}))
|
||||
MessageState.set(messages => [...messages, ...newMessages])
|
||||
MessageState.set((messages: Message[]) => [...messages, ...newMessages])
|
||||
}
|
||||
})
|
||||
|
||||
type PaginateCacheType = {
|
||||
keyArgs: string[]
|
||||
merge: FieldMergeFunction
|
||||
}
|
||||
|
||||
// Modified version of Apollo's offsetLimitPagination()
|
||||
const paginateCache = keyArgs => ({
|
||||
keyArgs,
|
||||
merge(existing, incoming, { args, fieldName }) {
|
||||
const merged = existing ? existing.slice(0) : []
|
||||
if (args?.paginate) {
|
||||
const { offset = 0 } = args.paginate
|
||||
for (let i = 0; i < incoming.length; ++i) {
|
||||
merged[offset + i] = incoming[i]
|
||||
const paginateCache = (keyArgs: string[]) =>
|
||||
({
|
||||
keyArgs,
|
||||
merge(existing, incoming, { args, fieldName }) {
|
||||
const merged = existing ? existing.slice(0) : []
|
||||
if (args?.paginate) {
|
||||
const { offset = 0 } = args.paginate
|
||||
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 {
|
||||
throw new Error(`Paginate argument is missing for query: ${fieldName}`)
|
||||
}
|
||||
return merged
|
||||
},
|
||||
})
|
||||
return merged
|
||||
},
|
||||
} as PaginateCacheType)
|
||||
|
||||
const memoryCache = new InMemoryCache({
|
||||
typePolicies: {
|
|
@ -3,29 +3,7 @@ import { authToken } from '../helpers/authentication'
|
|||
import { Checkbox, Dropdown, Button, Icon } from 'semantic-ui-react'
|
||||
import styled from 'styled-components'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const sortingOptions = [
|
||||
{
|
||||
key: 'date_shot',
|
||||
value: 'date_shot',
|
||||
text: 'Date shot',
|
||||
},
|
||||
{
|
||||
key: 'updated_at',
|
||||
value: 'updated_at',
|
||||
text: 'Date imported',
|
||||
},
|
||||
{
|
||||
key: 'title',
|
||||
value: 'title',
|
||||
text: 'Title',
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
value: 'type',
|
||||
text: 'Kind',
|
||||
},
|
||||
]
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const FavoritesCheckboxStyle = styled(Checkbox)`
|
||||
margin-bottom: 16px;
|
||||
|
@ -56,14 +34,18 @@ const FavoritesCheckboxStyle = styled(Checkbox)`
|
|||
}
|
||||
`
|
||||
|
||||
export const FavoritesCheckbox = ({ onlyFavorites, setOnlyFavorites }) => (
|
||||
<FavoritesCheckboxStyle
|
||||
toggle
|
||||
label="Show only favorites"
|
||||
checked={onlyFavorites}
|
||||
onChange={(e, result) => setOnlyFavorites(result.checked)}
|
||||
/>
|
||||
)
|
||||
export const FavoritesCheckbox = ({ onlyFavorites, setOnlyFavorites }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<FavoritesCheckboxStyle
|
||||
toggle
|
||||
label={t('album_filter.only_favorites', 'Show only favorites')}
|
||||
checked={onlyFavorites}
|
||||
onChange={(e, result) => setOnlyFavorites(result.checked)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
FavoritesCheckbox.propTypes = {
|
||||
onlyFavorites: PropTypes.bool.isRequired,
|
||||
|
@ -75,17 +57,46 @@ const OrderDirectionButton = styled(Button)`
|
|||
margin-left: 10px !important;
|
||||
`
|
||||
|
||||
const SortByLabel = styled.strong`
|
||||
margin-left: 4px;
|
||||
margin-right: 6px;
|
||||
`
|
||||
|
||||
const AlbumFilter = ({
|
||||
onlyFavorites,
|
||||
setOnlyFavorites,
|
||||
setOrdering,
|
||||
ordering,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const onChangeOrderDirection = (e, data) => {
|
||||
const direction = data.children.props.name === 'arrow up' ? 'DESC' : 'ASC'
|
||||
setOrdering({ orderDirection: direction })
|
||||
}
|
||||
|
||||
const sortingOptions = [
|
||||
{
|
||||
key: 'date_shot',
|
||||
value: 'date_shot',
|
||||
text: t('album_filter.sorting_options.date_shot', 'Date shot'),
|
||||
},
|
||||
{
|
||||
key: 'updated_at',
|
||||
value: 'updated_at',
|
||||
text: t('album_filter.sorting_options.date_imported', 'Date imported'),
|
||||
},
|
||||
{
|
||||
key: 'title',
|
||||
value: 'title',
|
||||
text: t('album_filter.sorting_options.title', 'Title'),
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
value: 'type',
|
||||
text: t('album_filter.sorting_options.type', 'Kind'),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
{authToken() && (
|
||||
|
@ -94,7 +105,7 @@ const AlbumFilter = ({
|
|||
setOnlyFavorites={setOnlyFavorites}
|
||||
/>
|
||||
)}
|
||||
<strong> Sort by </strong>
|
||||
<SortByLabel>{t('album_filter.sort_by', 'Sort by')}</SortByLabel>
|
||||
<Dropdown
|
||||
selection
|
||||
options={sortingOptions}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useContext } from 'react'
|
||||
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 styled from 'styled-components'
|
||||
import { Icon } from 'semantic-ui-react'
|
||||
|
@ -8,6 +8,7 @@ import { SidebarContext } from './sidebar/Sidebar'
|
|||
import AlbumSidebar from './sidebar/AlbumSidebar'
|
||||
import { useLazyQuery, gql } from '@apollo/client'
|
||||
import { authToken } from '../helpers/authentication'
|
||||
import { albumPathQuery } from './__generated__/albumPathQuery'
|
||||
|
||||
const Header = styled.h1`
|
||||
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} />
|
||||
}
|
||||
|
||||
|
@ -48,8 +49,18 @@ const ALBUM_PATH_QUERY = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const AlbumTitle = ({ album, disableLink = false }) => {
|
||||
const [fetchPath, { data: pathData }] = useLazyQuery(ALBUM_PATH_QUERY)
|
||||
type AlbumTitleProps = {
|
||||
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)
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -68,10 +79,7 @@ const AlbumTitle = ({ album, disableLink = false }) => {
|
|||
|
||||
let title = <span>{album.title}</span>
|
||||
|
||||
let path = []
|
||||
if (pathData) {
|
||||
path = pathData.album.path
|
||||
}
|
||||
const path = pathData?.album.path || []
|
||||
|
||||
const breadcrumbSections = path
|
||||
.slice()
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue