mirror of
https://github.com/photoprism/photoprism.git
synced 2026-01-23 02:24:24 +00:00
Merge branch 'develop' into feature/custom-tf-model-127
This commit is contained in:
commit
e704ccfc47
475 changed files with 45088 additions and 27486 deletions
|
|
@ -7,6 +7,10 @@
|
|||
/assets/facenet
|
||||
/assets/nasnet
|
||||
/assets/nsfw
|
||||
/assets/efficientnet
|
||||
/assets/imagenet
|
||||
/assets/resnet
|
||||
/assets/vision
|
||||
/storage
|
||||
/build
|
||||
/photoprism
|
||||
|
|
@ -31,6 +35,10 @@ compose.*.yaml
|
|||
*.override.yaml
|
||||
*.tmp.yml
|
||||
*.tmp.yaml
|
||||
*.tmp
|
||||
*.img
|
||||
*.img.xz
|
||||
*.img.gz
|
||||
|
||||
# Automatically generated files, e.g. by editors and operating systems
|
||||
.DS_Store
|
||||
|
|
|
|||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -18,6 +18,10 @@
|
|||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.tmp
|
||||
*.img
|
||||
*.img.xz
|
||||
*.img.gz
|
||||
/*.zip
|
||||
/coverage.*
|
||||
__pycache__
|
||||
|
|
@ -40,6 +44,7 @@ venv
|
|||
/frontend/src/locales/*.mo
|
||||
/frontend/tests_output
|
||||
frontend/coverage/
|
||||
**/__screenshots__/
|
||||
/photoprism
|
||||
/photoprism-*
|
||||
/photos/originals/*
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Ubuntu 25.04 (Plucky Puffin)
|
||||
FROM photoprism/develop:250507-plucky
|
||||
FROM photoprism/develop:250709-plucky
|
||||
|
||||
## Alternative Environments:
|
||||
# FROM photoprism/develop:armv7 # ARMv7 (32bit)
|
||||
|
|
@ -20,3 +20,5 @@ WORKDIR "/go/src/github.com/photoprism/photoprism"
|
|||
# Copy source to image.
|
||||
COPY . .
|
||||
COPY --chown=root:root /scripts/dist/ /scripts/
|
||||
|
||||
RUN sudo /scripts/install-yt-dlp.sh
|
||||
29
Makefile
29
Makefile
|
|
@ -74,6 +74,7 @@ test: test-js test-go
|
|||
test-go: reset-sqlite run-test-go
|
||||
test-pkg: reset-sqlite run-test-pkg
|
||||
test-api: reset-sqlite run-test-api
|
||||
test-video: reset-sqlite run-test-video
|
||||
test-entity: reset-sqlite run-test-entity
|
||||
test-commands: reset-sqlite run-test-commands
|
||||
test-photoprism: reset-sqlite run-test-photoprism
|
||||
|
|
@ -239,12 +240,15 @@ dep-npm:
|
|||
sudo npm install -g npm
|
||||
dep-js:
|
||||
(cd frontend && npm ci --no-update-notifier --no-audit)
|
||||
# TODO: If in the future we want to test in a real browser environment, add this (Playwright)
|
||||
# (cd frontend && npx playwright install chromium)
|
||||
dep-go:
|
||||
go build -v ./...
|
||||
dep-upgrade:
|
||||
go get -u -t ./...
|
||||
dep-upgrade-js:
|
||||
(cd frontend && npm update --legacy-peer-deps)
|
||||
frontend-update:
|
||||
make -C frontend update
|
||||
dep-upgrade-js: frontend-update
|
||||
dep-tensorflow:
|
||||
scripts/download-facenet.sh
|
||||
scripts/download-nasnet.sh
|
||||
|
|
@ -320,6 +324,9 @@ docker-tensorflow-arm64:
|
|||
terminal-tensorflow-arm64:
|
||||
mkdir -p ./build
|
||||
docker run --rm --pull missing -ti --platform=arm64 -v "./build:/build" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/tensorflow:arm64 bash
|
||||
build-setup: build-setup-nas-raspberry-pi
|
||||
build-setup-nas-raspberry-pi:
|
||||
./scripts/setup/nas/raspberry-pi/build.sh
|
||||
watch-js:
|
||||
(cd frontend && env BUILD_ENV=development NODE_ENV=production npm run watch)
|
||||
test-js:
|
||||
|
|
@ -343,6 +350,21 @@ acceptance-auth-short:
|
|||
acceptance-auth-firefox:
|
||||
$(info Running JS acceptance-auth tests in Firefox...)
|
||||
(cd frontend && npm run testcafe -- firefox:headless --test-grep "^(Common|Core)\:*" --test-meta mode=auth --config-file ./testcaferc.json --disable-native-automation "tests/acceptance")
|
||||
vitest:
|
||||
$(info Running Vitest unit tests...)
|
||||
(cd frontend && npm run vitest)
|
||||
vitest-watch:
|
||||
$(info Running Vitest unit tests in watch mode...)
|
||||
(cd frontend && npm run vitest-watch)
|
||||
vitest-coverage:
|
||||
$(info Running Vitest unit tests with coverage...)
|
||||
(cd frontend && npm run vitest-coverage)
|
||||
vitest-component:
|
||||
$(info Running Vitest component tests...)
|
||||
(cd frontend && npm run vitest-component)
|
||||
vitest-ui:
|
||||
$(info Opening Vitest UI...)
|
||||
(cd frontend && npm run vitest-ui)
|
||||
reset-mariadb:
|
||||
$(info Resetting photoprism database...)
|
||||
mysql < scripts/sql/reset-photoprism.sql
|
||||
|
|
@ -376,6 +398,9 @@ run-test-pkg:
|
|||
run-test-api:
|
||||
$(info Running all API tests...)
|
||||
$(GOTEST) -parallel 2 -count 1 -cpu 2 -tags="slow,develop" -timeout 20m ./internal/api/...
|
||||
run-test-video:
|
||||
$(info Running all video tests...)
|
||||
$(GOTEST) -parallel 2 -count 1 -cpu 2 -tags="slow,develop" -timeout 20m ./internal/ffmpeg/... ./internal/photoprism/dl/... ./pkg/media/...
|
||||
run-test-entity:
|
||||
$(info Running all Entity tests...)
|
||||
$(GOTEST) -parallel 2 -count 1 -cpu 2 -tags="slow,develop" -timeout 20m ./internal/entity/...
|
||||
|
|
|
|||
222
NOTICE
222
NOTICE
|
|
@ -9,7 +9,7 @@ The following 3rd-party software packages may be used by or distributed with
|
|||
PhotoPrism. Any information relevant to third-party vendors listed below are
|
||||
collected using common, reasonable means.
|
||||
|
||||
Date generated: 2025-05-06
|
||||
Date generated: 2025-07-16
|
||||
|
||||
================================================================================
|
||||
|
||||
|
|
@ -408,8 +408,8 @@ SOFTWARE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/cpuguy83/go-md2man/v2/md2man
|
||||
Version: v2.0.6
|
||||
License: MIT (https://github.com/cpuguy83/go-md2man/blob/v2.0.6/LICENSE.md)
|
||||
Version: v2.0.7
|
||||
License: MIT (https://github.com/cpuguy83/go-md2man/blob/v2.0.7/LICENSE.md)
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
|
|
@ -541,7 +541,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
|||
|
||||
Package: github.com/dsoprea/go-heic-exif-extractor/v2
|
||||
Version: v2.0.0-20210512044107-62067e44c235
|
||||
License: MIT (https://github.com/dsoprea/go-heic-exif-extractor/blob/62067e44c235/LICENSE)
|
||||
License: MIT (https://github.com/dsoprea/go-heic-exif-extractor/blob/62067e44c235/v2/LICENSE)
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
|
|
@ -585,7 +585,7 @@ SOFTWARE.
|
|||
|
||||
Package: github.com/dsoprea/go-jpeg-image-structure/v2
|
||||
Version: v2.0.0-20221012074422-4f3f7e934102
|
||||
License: MIT (https://github.com/dsoprea/go-jpeg-image-structure/blob/4f3f7e934102/LICENSE)
|
||||
License: MIT (https://github.com/dsoprea/go-jpeg-image-structure/blob/4f3f7e934102/v2/LICENSE)
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
|
|
@ -661,7 +661,7 @@ SOFTWARE.
|
|||
|
||||
Package: github.com/dsoprea/go-png-image-structure/v2
|
||||
Version: v2.0.0-20210512210324-29b889a6093d
|
||||
License: MIT (https://github.com/dsoprea/go-png-image-structure/blob/29b889a6093d/LICENSE)
|
||||
License: MIT (https://github.com/dsoprea/go-png-image-structure/blob/29b889a6093d/v2/LICENSE)
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
|
|
@ -677,7 +677,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
|||
|
||||
Package: github.com/dsoprea/go-tiff-image-structure/v2
|
||||
Version: v2.0.0-20221003165014-8ecc4f52edca
|
||||
License: MIT (https://github.com/dsoprea/go-tiff-image-structure/blob/8ecc4f52edca/LICENSE)
|
||||
License: MIT (https://github.com/dsoprea/go-tiff-image-structure/blob/8ecc4f52edca/v2/LICENSE)
|
||||
|
||||
MIT License
|
||||
|
||||
|
|
@ -705,7 +705,7 @@ SOFTWARE.
|
|||
|
||||
Package: github.com/dsoprea/go-utility/v2
|
||||
Version: v2.0.0-20221003172846-a3e1774ef349
|
||||
License: MIT (https://github.com/dsoprea/go-utility/blob/a3e1774ef349/LICENSE)
|
||||
License: MIT (https://github.com/dsoprea/go-utility/blob/a3e1774ef349/v2/LICENSE)
|
||||
|
||||
Copyright 2019 Random Ingenuity InformationWorks
|
||||
|
||||
|
|
@ -1010,6 +1010,33 @@ SOFTWARE.
|
|||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/fatih/color
|
||||
Version: v1.18.0
|
||||
License: MIT (https://github.com/fatih/color/blob/v1.18.0/LICENSE.md)
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Fatih Arslan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/gabriel-vasile/mimetype
|
||||
Version: v1.4.9
|
||||
License: MIT (https://github.com/gabriel-vasile/mimetype/blob/v1.4.9/LICENSE)
|
||||
|
|
@ -1095,8 +1122,8 @@ THE SOFTWARE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/gin-gonic/gin
|
||||
Version: v1.10.0
|
||||
License: MIT (https://github.com/gin-gonic/gin/blob/v1.10.0/LICENSE)
|
||||
Version: v1.10.1
|
||||
License: MIT (https://github.com/gin-gonic/gin/blob/v1.10.1/LICENSE)
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
|
|
@ -1123,8 +1150,8 @@ THE SOFTWARE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/go-co-op/gocron/v2
|
||||
Version: v2.16.1
|
||||
License: MIT (https://github.com/go-co-op/gocron/blob/v2.16.1/LICENSE)
|
||||
Version: v2.16.2
|
||||
License: MIT (https://github.com/go-co-op/gocron/blob/v2.16.2/LICENSE)
|
||||
|
||||
MIT License
|
||||
|
||||
|
|
@ -2316,8 +2343,8 @@ SOFTWARE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/golang/geo
|
||||
Version: v0.0.0-20250505201543-5b58c72585db
|
||||
License: Apache-2.0 (https://github.com/golang/geo/blob/5b58c72585db/LICENSE)
|
||||
Version: v0.0.0-20250707181242-c5087ca84cf4
|
||||
License: Apache-2.0 (https://github.com/golang/geo/blob/c5087ca84cf4/LICENSE)
|
||||
|
||||
|
||||
Apache License
|
||||
|
|
@ -2525,8 +2552,8 @@ License: Apache-2.0 (https://github.com/golang/geo/blob/5b58c72585db/LICENSE)
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/google/open-location-code/go
|
||||
Version: v0.0.0-20250415120251-fa6d7f9d4765
|
||||
License: Apache-2.0 (https://github.com/google/open-location-code/blob/fa6d7f9d4765/go/LICENSE)
|
||||
Version: v0.0.0-20250620134813-83986da0156b
|
||||
License: Apache-2.0 (https://github.com/google/open-location-code/blob/83986da0156b/go/LICENSE)
|
||||
|
||||
|
||||
Apache License
|
||||
|
|
@ -3717,8 +3744,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/klauspost/cpuid/v2
|
||||
Version: v2.2.10
|
||||
License: MIT (https://github.com/klauspost/cpuid/blob/v2.2.10/LICENSE)
|
||||
Version: v2.3.0
|
||||
License: MIT (https://github.com/klauspost/cpuid/blob/v2.3.0/LICENSE)
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
|
|
@ -3982,8 +4009,8 @@ SOFTWARE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/leonelquinteros/gotext
|
||||
Version: v1.7.1
|
||||
License: MIT (https://github.com/leonelquinteros/gotext/blob/v1.7.1/LICENSE)
|
||||
Version: v1.7.2
|
||||
License: MIT (https://github.com/leonelquinteros/gotext/blob/v1.7.2/LICENSE)
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
|
|
@ -4149,6 +4176,34 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/mattn/go-colorable
|
||||
Version: v0.1.14
|
||||
License: MIT (https://github.com/mattn/go-colorable/blob/v0.1.14/LICENSE)
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Yasuhiro Matsumoto
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/mattn/go-isatty
|
||||
Version: v0.0.20
|
||||
License: MIT (https://github.com/mattn/go-isatty/blob/v0.0.20/LICENSE)
|
||||
|
|
@ -4318,9 +4373,65 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/olekukonko/errors
|
||||
Version: v1.1.0
|
||||
License: MIT (https://github.com/olekukonko/errors/blob/v1.1.0/LICENSE)
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Oleku Konko
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/olekukonko/ll
|
||||
Version: v0.0.9
|
||||
License: MIT (https://github.com/olekukonko/ll/blob/v0.0.9/LICENSE)
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Oleku Konko
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/olekukonko/tablewriter
|
||||
Version: v0.0.5
|
||||
License: MIT (https://github.com/olekukonko/tablewriter/blob/v0.0.5/LICENSE.md)
|
||||
Version: v1.0.8
|
||||
License: MIT (https://github.com/olekukonko/tablewriter/blob/v1.0.8/LICENSE.md)
|
||||
|
||||
Copyright (C) 2014 by Oleku Konko
|
||||
|
||||
|
|
@ -4461,8 +4572,8 @@ SOFTWARE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/pquerna/otp
|
||||
Version: v1.4.0
|
||||
License: Apache-2.0 (https://github.com/pquerna/otp/blob/v1.4.0/LICENSE)
|
||||
Version: v1.5.0
|
||||
License: Apache-2.0 (https://github.com/pquerna/otp/blob/v1.5.0/LICENSE)
|
||||
|
||||
|
||||
Apache License
|
||||
|
|
@ -4878,8 +4989,8 @@ License: Apache-2.0 (https://github.com/prometheus/client_golang/blob/v1.22.0/LI
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/prometheus/client_model/go
|
||||
Version: v0.6.1
|
||||
License: Apache-2.0 (https://github.com/prometheus/client_model/blob/v0.6.1/LICENSE)
|
||||
Version: v0.6.2
|
||||
License: Apache-2.0 (https://github.com/prometheus/client_model/blob/v0.6.2/LICENSE)
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
|
|
@ -5086,8 +5197,8 @@ License: Apache-2.0 (https://github.com/prometheus/client_model/blob/v0.6.1/LICE
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/prometheus/common
|
||||
Version: v0.63.0
|
||||
License: Apache-2.0 (https://github.com/prometheus/common/blob/v0.63.0/LICENSE)
|
||||
Version: v0.65.0
|
||||
License: Apache-2.0 (https://github.com/prometheus/common/blob/v0.65.0/LICENSE)
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
|
|
@ -5773,8 +5884,8 @@ SOFTWARE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/ugorji/go/codec
|
||||
Version: v1.2.12
|
||||
License: MIT (https://github.com/ugorji/go/blob/codec/v1.2.12/codec/LICENSE)
|
||||
Version: v1.2.14
|
||||
License: MIT (https://github.com/ugorji/go/blob/codec/v1.2.14/codec/LICENSE)
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
|
|
@ -5831,8 +5942,8 @@ SOFTWARE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/urfave/cli/v2
|
||||
Version: v2.27.6
|
||||
License: MIT (https://github.com/urfave/cli/blob/v2.27.6/LICENSE)
|
||||
Version: v2.27.7
|
||||
License: MIT (https://github.com/urfave/cli/blob/v2.27.7/LICENSE)
|
||||
|
||||
MIT License
|
||||
|
||||
|
|
@ -6302,9 +6413,9 @@ License: Apache-2.0 (https://github.com/zitadel/logging/blob/v0.6.2/LICENSE)
|
|||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/zitadel/oidc/v3/pkg
|
||||
Version: v3.38.1
|
||||
License: Apache-2.0 (https://github.com/zitadel/oidc/blob/v3.38.1/LICENSE)
|
||||
Package: github.com/zitadel/oidc/v3
|
||||
Version: v3.41.0
|
||||
License: Apache-2.0 (https://github.com/zitadel/oidc/blob/v3.41.0/LICENSE)
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
|
|
@ -7586,8 +7697,8 @@ License: Apache-2.0 (https://github.com/go4org/go4/blob/214862532bf5/LICENSE)
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: golang.org/x/crypto
|
||||
Version: v0.38.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/crypto/+/v0.38.0:LICENSE)
|
||||
Version: v0.40.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/crypto/+/v0.40.0:LICENSE)
|
||||
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
|
|
@ -7620,8 +7731,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: golang.org/x/image
|
||||
Version: v0.27.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/image/+/v0.27.0:LICENSE)
|
||||
Version: v0.29.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/image/+/v0.29.0:LICENSE)
|
||||
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
|
|
@ -7654,8 +7765,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: golang.org/x/mod/semver
|
||||
Version: v0.24.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/mod/+/v0.24.0:LICENSE)
|
||||
Version: v0.26.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/mod/+/v0.26.0:LICENSE)
|
||||
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
|
|
@ -7688,8 +7799,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: golang.org/x/net
|
||||
Version: v0.40.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/net/+/v0.40.0:LICENSE)
|
||||
Version: v0.42.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/net/+/v0.42.0:LICENSE)
|
||||
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
|
|
@ -7722,8 +7833,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: golang.org/x/oauth2
|
||||
Version: v0.29.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/oauth2/+/v0.29.0:LICENSE)
|
||||
Version: v0.30.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/oauth2/+/v0.30.0:LICENSE)
|
||||
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
|
|
@ -7756,8 +7867,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: golang.org/x/sync/errgroup
|
||||
Version: v0.14.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/sync/+/v0.14.0:LICENSE)
|
||||
Version: v0.16.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/sync/+/v0.16.0:LICENSE)
|
||||
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
|
|
@ -7790,8 +7901,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: golang.org/x/sys
|
||||
Version: v0.33.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/sys/+/v0.33.0:LICENSE)
|
||||
Version: v0.34.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/sys/+/v0.34.0:LICENSE)
|
||||
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
|
|
@ -7824,8 +7935,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: golang.org/x/text
|
||||
Version: v0.25.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/text/+/v0.25.0:LICENSE)
|
||||
Version: v0.27.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/text/+/v0.27.0:LICENSE)
|
||||
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
|
|
@ -7858,8 +7969,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: golang.org/x/time/rate
|
||||
Version: v0.11.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/time/+/v0.11.0:LICENSE)
|
||||
Version: v0.12.0
|
||||
License: BSD-3-Clause (https://cs.opensource.google/go/x/time/+/v0.12.0:LICENSE)
|
||||
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
|
|
@ -8241,10 +8352,13 @@ Package License Copyright
|
|||
@testing-library/jest-dom MIT Ernesto Garcia <gnapse@gmail.com> (http://gnapse.github.io)
|
||||
@testing-library/react MIT Kent C. Dodds <me@kentcdodds.com> (https://kentcdodds.com)
|
||||
@vitejs/plugin-react MIT Evan You
|
||||
@vitejs/plugin-vue MIT Evan You
|
||||
@vitest/browser MIT n/a
|
||||
@vitest/coverage-v8 MIT Anthony Fu <anthonyfu117@hotmail.com>
|
||||
@vitest/ui MIT n/a
|
||||
@vue/compiler-sfc MIT Evan You
|
||||
@vue/language-server MIT n/a
|
||||
@vue/test-utils MIT Lachlan Miller lachlan.miller.1990@outlook.com
|
||||
@vvo/tzdb MIT Vincent Voyer <vincent@codeagain.com>
|
||||
axios MIT Matt Zabriskie
|
||||
axios-mock-adapter MIT Colin Timmermans <colintimmermans@gmail.com>
|
||||
|
|
@ -8267,7 +8381,6 @@ eslint-plugin-html ISC n/a
|
|||
eslint-plugin-import MIT Ben Mosher <me@benmosher.com>
|
||||
eslint-plugin-node MIT Toru Nagashima
|
||||
eslint-plugin-prettier MIT Teddy Katz
|
||||
eslint-plugin-promise ISC jden <jason@denizac.org>
|
||||
eslint-plugin-vue MIT Toru Nagashima (https://github.com/mysticatea)
|
||||
eslint-plugin-vuetify MIT Kael Watts-Deuchar <kaelwd@gmail.com>
|
||||
eslint-webpack-plugin MIT Ricardo Gobbo de Souza <ricardogobbosouza@yahoo.com.br>
|
||||
|
|
@ -8295,6 +8408,7 @@ mocha MIT TJ Holowaychuk <tj@vision-media.
|
|||
node-storage-shim ISC Michael Nahkies
|
||||
passive-events-support MIT Ignas Damunskis <ignas3run@gmail.com>
|
||||
photoswipe MIT Dmytro Semenov (https://dimsemenov.com)
|
||||
playwright Apache-2.0 Microsoft Corporation
|
||||
postcss MIT Andrey Sitnik <andrey@sitnik.ru>
|
||||
postcss-import MIT Maxime Thirouin
|
||||
postcss-loader MIT Andrey Sitnik <andrey@sitnik.ru>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ You are [welcome to contact us](https://www.photoprism.app/contact) for change r
|
|||
|
||||
**[Andreas Krizek](https://github.com/Cosmic314)** (January 2025)
|
||||
|
||||
**[Jason Grim](https://github.com/jgrim)** (June 2025)
|
||||
|
||||
## Gold Sponsors ##
|
||||
|
||||
[**Simen Eriksen**](https://github.com/dennorske) (GitHub Sponsors, December 2019)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
examples
|
||||
efficientnet
|
||||
imagenet
|
||||
resnet
|
||||
vision
|
||||
README.md
|
||||
docs
|
||||
.*
|
||||
BIN
assets/examples/bear.m2ts
Normal file
BIN
assets/examples/bear.m2ts
Normal file
Binary file not shown.
BIN
assets/examples/m2ts.mp4
Normal file
BIN
assets/examples/m2ts.mp4
Normal file
Binary file not shown.
|
|
@ -294,7 +294,7 @@ msgstr "Importación cancelada"
|
|||
#: messages.go:172
|
||||
#, c-format
|
||||
msgid "Indexing completed in %d s"
|
||||
msgstr "Indexación completada em %d"
|
||||
msgstr "Indexación completada en %d"
|
||||
|
||||
#: messages.go:173
|
||||
msgid "Indexing originals..."
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: ci@photoprism.app\n"
|
||||
"POT-Creation-Date: 2025-03-11 19:22+0000\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-03-15 12:54+0000\n"
|
||||
"PO-Revision-Date: 2025-05-12 23:50+0000\n"
|
||||
"Last-Translator: Admin <hello@photoprism.app>\n"
|
||||
"Language-Team: Japanese <https://translate.photoprism.app/projects/"
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ services:
|
|||
PHOTOPRISM_PASSWORD_RESET_URI: "https://keycloak.localssl.dev/realms/master/login-actions/reset-credentials"
|
||||
PHOTOPRISM_USAGE_INFO: "true"
|
||||
PHOTOPRISM_FILES_QUOTA: "100"
|
||||
## Customization:
|
||||
PHOTOPRISM_DEFAULT_LOCALE: "en" # default user interface language, e.g. "en" or "de"
|
||||
PHOTOPRISM_PLACES_LOCALE: "local" # location details language, e.g. "local", "en", or
|
||||
## OpenID Connect (pre-configured for local tests):
|
||||
## see https://keycloak.localssl.dev/realms/master/.well-known/openid-configuration
|
||||
PHOTOPRISM_OIDC_URI: "https://keycloak.localssl.dev/realms/master"
|
||||
|
|
@ -115,8 +118,8 @@ services:
|
|||
PHOTOPRISM_FFMPEG_ENCODER: "intel" # H.264/AVC encoder (software, intel, nvidia, apple, raspberry, or vaapi)
|
||||
PHOTOPRISM_FFMPEG_SIZE: "1920" # video size limit in pixels (720-7680) (default: 3840)
|
||||
# PHOTOPRISM_FFMPEG_BITRATE: "60" # video bitrate limit in Mbps (default: 60)
|
||||
## Run/install on first startup (options: update tensorflow https intel gpu davfs):
|
||||
PHOTOPRISM_INIT: "https intel tensorflow"
|
||||
## Run/install on first startup (options: update tensorflow https intel gpu davfs yt-dlp):
|
||||
PHOTOPRISM_INIT: "https intel tensorflow yt-dlp"
|
||||
## Share hardware devices with FFmpeg for hardware video transcoding:
|
||||
devices:
|
||||
- "/dev/dri:/dev/dri"
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ services:
|
|||
PHOTOPRISM_PASSWORD_RESET_URI: "https://keycloak.localssl.dev/realms/master/login-actions/reset-credentials"
|
||||
PHOTOPRISM_USAGE_INFO: "true"
|
||||
PHOTOPRISM_FILES_QUOTA: "100"
|
||||
## Customization:
|
||||
PHOTOPRISM_DEFAULT_LOCALE: "en" # default user interface language, e.g. "en" or "de"
|
||||
PHOTOPRISM_PLACES_LOCALE: "local" # location details language, e.g. "local", "en", or
|
||||
## OpenID Connect (pre-configured for local tests):
|
||||
## see https://keycloak.localssl.dev/realms/master/.well-known/openid-configuration
|
||||
PHOTOPRISM_OIDC_URI: "https://keycloak.localssl.dev/realms/master"
|
||||
|
|
@ -114,14 +117,14 @@ services:
|
|||
PHOTOPRISM_THUMB_FILTER: "auto" # downscaling filter (imaging best to worst: blackman, lanczos, cubic, linear, nearest)
|
||||
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
|
||||
TF_CPP_MIN_LOG_LEVEL: 1 # show TensorFlow log messages for development
|
||||
## Run/install on first startup, see https://github.com/photoprism/photoprism/blob/develop/scripts/dist/Makefile:
|
||||
PHOTOPRISM_INIT: "https tensorflow-gpu" # common options: update https tensorflow tensorflow-gpu intel gpu davfs
|
||||
## Nvidia Video Transcoding (https://docs.photoprism.app/getting-started/advanced/transcoding/#nvidia-container-toolkit):
|
||||
NVIDIA_VISIBLE_DEVICES: "all"
|
||||
NVIDIA_DRIVER_CAPABILITIES: "all"
|
||||
PHOTOPRISM_FFMPEG_ENCODER: "nvidia" # H.264/AVC encoder (software, intel, nvidia, apple, raspberry, or vaapi)
|
||||
PHOTOPRISM_FFMPEG_SIZE: "1920" # video size limit in pixels (720-7680) (default: 3840)
|
||||
# PHOTOPRISM_FFMPEG_BITRATE: "60" # video bitrate limit in Mbps (default: 60)
|
||||
## Run/install on first startup (options: update tensorflow https intel gpu davfs yt-dlp):
|
||||
PHOTOPRISM_INIT: "https tensorflow-gpu yt-dlp"
|
||||
## Share hardware devices with FFmpeg and TensorFlow (optional):
|
||||
# devices:
|
||||
# - "/dev/dri:/dev/dri" # Intel QSV (Broadwell and later) or VAAPI (Haswell and earlier)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ services:
|
|||
- "go-mod:/go/pkg/mod"
|
||||
shm_size: "2gb"
|
||||
environment:
|
||||
PHOTOPRISM_INIT: "https"
|
||||
PHOTOPRISM_ADMIN_USER: "admin" # admin login username
|
||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial admin password (8-72 characters)
|
||||
PHOTOPRISM_AUTH_MODE: "password" # authentication mode (public, password)
|
||||
|
|
@ -66,7 +65,8 @@ services:
|
|||
PHOTOPRISM_THUMB_SIZE_UNCACHED: 7680 # on-demand rendering size limit (default 7680, min 720, max 7680)
|
||||
PHOTOPRISM_JPEG_SIZE: 7680 # size limit for converted image files in pixels (720-30000)
|
||||
TF_CPP_MIN_LOG_LEVEL: 1 # show TensorFlow log messages for development
|
||||
|
||||
## Run/install on first startup (options: update tensorflow https intel gpu davfs yt-dlp):
|
||||
PHOTOPRISM_INIT: "https yt-dlp"
|
||||
## PostgreSQL Database Server
|
||||
## Docs: https://www.postgresql.org/docs/
|
||||
postgres:
|
||||
|
|
|
|||
|
|
@ -57,6 +57,9 @@ services:
|
|||
PHOTOPRISM_PASSWORD_RESET_URI: "https://keycloak.localssl.dev/realms/master/login-actions/reset-credentials"
|
||||
PHOTOPRISM_USAGE_INFO: "true"
|
||||
PHOTOPRISM_FILES_QUOTA: "100"
|
||||
## Customization:
|
||||
PHOTOPRISM_DEFAULT_LOCALE: "en" # default user interface language, e.g. "en" or "de"
|
||||
PHOTOPRISM_PLACES_LOCALE: "local" # location details language, e.g. "local", "en", or "de"
|
||||
## OpenID Connect (pre-configured for local tests):
|
||||
## see https://keycloak.localssl.dev/realms/master/.well-known/openid-configuration
|
||||
PHOTOPRISM_OIDC_URI: "https://keycloak.localssl.dev/realms/master"
|
||||
|
|
@ -124,8 +127,8 @@ services:
|
|||
# LIBVA_DRIVER_NAME: "i965" # For Intel architectures Haswell and older which do not support QSV yet but use VAAPI instead
|
||||
PHOTOPRISM_FFMPEG_SIZE: "1920" # video size limit in pixels (720-7680) (default: 3840)
|
||||
# PHOTOPRISM_FFMPEG_BITRATE: "60" # video bitrate limit in Mbps (default: 60)
|
||||
## Run/install on first startup (options: update tensorflow https intel gpu davfs):
|
||||
PHOTOPRISM_INIT: "https"
|
||||
## Run/install on first startup (options: update tensorflow https intel gpu davfs yt-dlp):
|
||||
PHOTOPRISM_INIT: "https yt-dlp"
|
||||
## Share hardware devices with FFmpeg and TensorFlow (optional):
|
||||
# devices:
|
||||
# - "/dev/dri:/dev/dri" # Intel QSV (Broadwell and later) or VAAPI (Haswell and earlier)
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
|
|||
# Update pre-installed packages.
|
||||
RUN apt-get update && \
|
||||
apt-get -qq dist-upgrade && \
|
||||
yt-dlp -U && \
|
||||
/scripts/cleanup.sh
|
||||
|
||||
# Set default working directory.
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
|
|||
# Update pre-installed packages.
|
||||
RUN apt-get update && \
|
||||
apt-get -qq dist-upgrade && \
|
||||
yt-dlp -U && \
|
||||
/scripts/cleanup.sh
|
||||
|
||||
# Set default working directory.
|
||||
|
|
|
|||
|
|
@ -17,10 +17,15 @@ install-testcafe:
|
|||
npm install -g testcafe@latest
|
||||
install-eslint:
|
||||
npm install -g eslint globals @eslint/eslintrc @eslint/js eslint-config-prettier eslint-formatter-pretty eslint-plugin-html eslint-plugin-import eslint-plugin-node eslint-plugin-prettier eslint-plugin-promise eslint-plugin-vue eslint-webpack-plugin vue-eslint-parser prettier
|
||||
install:
|
||||
npm-install:
|
||||
$(info Installing dependencies...)
|
||||
npm install --no-update-notifier --no-audit
|
||||
update:
|
||||
npm update
|
||||
install: npm-install
|
||||
npm-update:
|
||||
$(info Updating dependencies...)
|
||||
npm update --save --package-lock
|
||||
update: npm-update npm-install
|
||||
upgrade: update
|
||||
watch:
|
||||
npm run watch
|
||||
build:
|
||||
|
|
@ -31,8 +36,6 @@ fmt:
|
|||
npm run fmt
|
||||
test:
|
||||
npm run test
|
||||
upgrade:
|
||||
npm run upgrade
|
||||
testcafe:
|
||||
npm run testcafe
|
||||
acceptance-local:
|
||||
|
|
|
|||
|
|
@ -13,10 +13,13 @@ Package License Copyright
|
|||
@testing-library/jest-dom MIT Ernesto Garcia <gnapse@gmail.com> (http://gnapse.github.io)
|
||||
@testing-library/react MIT Kent C. Dodds <me@kentcdodds.com> (https://kentcdodds.com)
|
||||
@vitejs/plugin-react MIT Evan You
|
||||
@vitejs/plugin-vue MIT Evan You
|
||||
@vitest/browser MIT n/a
|
||||
@vitest/coverage-v8 MIT Anthony Fu <anthonyfu117@hotmail.com>
|
||||
@vitest/ui MIT n/a
|
||||
@vue/compiler-sfc MIT Evan You
|
||||
@vue/language-server MIT n/a
|
||||
@vue/test-utils MIT Lachlan Miller lachlan.miller.1990@outlook.com
|
||||
@vvo/tzdb MIT Vincent Voyer <vincent@codeagain.com>
|
||||
axios MIT Matt Zabriskie
|
||||
axios-mock-adapter MIT Colin Timmermans <colintimmermans@gmail.com>
|
||||
|
|
@ -39,7 +42,6 @@ eslint-plugin-html ISC n/a
|
|||
eslint-plugin-import MIT Ben Mosher <me@benmosher.com>
|
||||
eslint-plugin-node MIT Toru Nagashima
|
||||
eslint-plugin-prettier MIT Teddy Katz
|
||||
eslint-plugin-promise ISC jden <jason@denizac.org>
|
||||
eslint-plugin-vue MIT Toru Nagashima (https://github.com/mysticatea)
|
||||
eslint-plugin-vuetify MIT Kael Watts-Deuchar <kaelwd@gmail.com>
|
||||
eslint-webpack-plugin MIT Ricardo Gobbo de Souza <ricardogobbosouza@yahoo.com.br>
|
||||
|
|
@ -67,6 +69,7 @@ mocha MIT TJ Holowaychuk <tj@vision-media.
|
|||
node-storage-shim ISC Michael Nahkies
|
||||
passive-events-support MIT Ignas Damunskis <ignas3run@gmail.com>
|
||||
photoswipe MIT Dmytro Semenov (https://dimsemenov.com)
|
||||
playwright Apache-2.0 Microsoft Corporation
|
||||
postcss MIT Andrey Sitnik <andrey@sitnik.ru>
|
||||
postcss-import MIT Maxime Thirouin
|
||||
postcss-loader MIT Andrey Sitnik <andrey@sitnik.ru>
|
||||
|
|
|
|||
|
|
@ -170,5 +170,5 @@ module.exports = (config) => {
|
|||
});
|
||||
|
||||
// Set default timezone.
|
||||
process.env.TZ = 'UTC';
|
||||
process.env.TZ = "UTC";
|
||||
};
|
||||
|
|
|
|||
7562
frontend/package-lock.json
generated
7562
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"name": "photoprism",
|
||||
"description": "AI-Powered Photos App",
|
||||
"author": "PhotoPrism UG",
|
||||
"license": "AGPL-3.0",
|
||||
"version": "1",
|
||||
"private": true,
|
||||
"description": "AI-Powered Photos App",
|
||||
"license": "AGPL-3.0",
|
||||
"author": "PhotoPrism UG",
|
||||
"scripts": {
|
||||
"acceptance-local": "testcafe chromium --selector-timeout 5000 -S -s tests/acceptance/screenshots tests/acceptance",
|
||||
"build": "webpack --node-env=production",
|
||||
|
|
@ -18,69 +18,71 @@
|
|||
"gettext-extract": "gettext-extract --output src/locales/translations.pot $(find ${SRC:-src} -type f \\( -iname \\*.vue -o -iname \\*.js \\) -not -path src/common/gettext.js)",
|
||||
"lint": "eslint --cache src/ *.js",
|
||||
"test": "karma start",
|
||||
"test-vitest": "vitest run",
|
||||
"test-vitest-watch": "vitest",
|
||||
"test-vitest-coverage": "vitest run --coverage",
|
||||
"test-vitest-component": "vitest run tests/vitest/component",
|
||||
"test-vitest-ui": "vitest --ui",
|
||||
"vitest": "env TZ=UTC vitest run",
|
||||
"vitest-watch": "env TZ=UTC vitest --watch",
|
||||
"vitest-coverage": "env TZ=UTC vitest run --coverage",
|
||||
"vitest-component": "env TZ=UTC vitest run tests/vitest/component",
|
||||
"vitest-ui": "env TZ=UTC vitest --ui --watch",
|
||||
"testcafe": "testcafe",
|
||||
"trace": "webpack --stats-children",
|
||||
"upgrade": "npm update && npm audit fix",
|
||||
"update": "npm update --save --package-lock && npm install --no-update-notifier --no-audit",
|
||||
"watch": "webpack --watch"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.25% and last 2 years"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/cli": "^7.27.1",
|
||||
"@babel/core": "^7.27.1",
|
||||
"@babel/plugin-transform-runtime": "^7.27.1",
|
||||
"@babel/preset-env": "^7.27.1",
|
||||
"@babel/cli": "^7.28.0",
|
||||
"@babel/core": "^7.28.0",
|
||||
"@babel/plugin-transform-runtime": "^7.28.0",
|
||||
"@babel/preset-env": "^7.28.0",
|
||||
"@babel/register": "^7.27.1",
|
||||
"@babel/runtime": "^7.27.1",
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@lcdp/offline-plugin": "^5.1.1",
|
||||
"@eslint/js": "9.31.0",
|
||||
"@lcdp/offline-plugin": "^5.1.3",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"@vitest/coverage-v8": "^3.1.3",
|
||||
"@vitest/ui": "^3.1.3",
|
||||
"@vue/compiler-sfc": "^3.5.13",
|
||||
"@vue/language-server": "^2.2.10",
|
||||
"@vvo/tzdb": "^6.161.0",
|
||||
"axios": "^1.9.0",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vitest/browser": "^3.2.4",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"@vue/compiler-sfc": "^3.5.17",
|
||||
"@vue/language-server": "^3.0.1",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vvo/tzdb": "^6.178.0",
|
||||
"axios": "^1.10.0",
|
||||
"axios-mock-adapter": "^2.1.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-istanbul": "^7.0.0",
|
||||
"babel-plugin-polyfill-corejs3": "^0.12.0",
|
||||
"browserslist": "^4.24.5",
|
||||
"chai": "^5.2.0",
|
||||
"babel-plugin-polyfill-corejs3": "^0.13.0",
|
||||
"browserslist": "^4.25.1",
|
||||
"chai": "^5.2.1",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"chrome-finder": "^1.0.7",
|
||||
"core-js": "^3.42.0",
|
||||
"core-js": "^3.44.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"cssnano": "^7.0.7",
|
||||
"cssnano": "^7.1.0",
|
||||
"easygettext": "^2.17.0",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-prettier": "^10.1.3",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-plugin-html": "^8.1.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-html": "^8.1.3",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.4.0",
|
||||
"eslint-plugin-promise": "^7.2.1",
|
||||
"eslint-plugin-vue": "^10.1.0",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"eslint-plugin-vue": "^10.3.0",
|
||||
"eslint-plugin-vuetify": "^2.5.2",
|
||||
"eslint-webpack-plugin": "^5.0.1",
|
||||
"eslint-webpack-plugin": "^5.0.2",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"file-loader": "^6.2.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"floating-vue": "^5.2.2",
|
||||
"globals": "^16.0.1",
|
||||
"hls.js": "^1.6.2",
|
||||
"globals": "^16.3.0",
|
||||
"hls.js": "^1.6.7",
|
||||
"i": "^0.3.7",
|
||||
"jsdom": "^26.1.0",
|
||||
"karma": "^6.4.4",
|
||||
|
|
@ -90,29 +92,30 @@
|
|||
"karma-mocha": "^2.0.1",
|
||||
"karma-verbose-reporter": "^0.0.8",
|
||||
"karma-webpack": "^5.0.1",
|
||||
"luxon": "^3.6.1",
|
||||
"maplibre-gl": "^5.5.0",
|
||||
"luxon": "^3.7.1",
|
||||
"maplibre-gl": "^5.6.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"minimist": ">=1.2.8",
|
||||
"mocha": "^11.2.2",
|
||||
"minimist": "^1.2.8",
|
||||
"mocha": "^11.7.1",
|
||||
"node-storage-shim": "^2.0.1",
|
||||
"passive-events-support": "^1.1.0",
|
||||
"photoswipe": "^5.4.4",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-import": "^16.1.0",
|
||||
"playwright": "^1.54.1",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-import": "^16.1.1",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"postcss-preset-env": "^10.1.6",
|
||||
"postcss-preset-env": "^10.2.4",
|
||||
"postcss-reporter": "^7.1.0",
|
||||
"postcss-url": "^10.1.3",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier": "^3.6.2",
|
||||
"pubsub-js": "^1.9.5",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"sanitize-html": "^2.16.0",
|
||||
"sass": "^1.87.0",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"sass": "^1.89.2",
|
||||
"sass-loader": "^16.0.5",
|
||||
"server": "^1.0.41",
|
||||
"server": "^1.0.42",
|
||||
"sockette": "^2.0.6",
|
||||
"style-loader": "^4.0.0",
|
||||
"svg-url-loader": "^8.0.0",
|
||||
|
|
@ -120,8 +123,8 @@
|
|||
"url-loader": "^4.1.1",
|
||||
"util": "^0.12.5",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.1.3",
|
||||
"vue": "^3.5.13",
|
||||
"vitest": "^3.2.4",
|
||||
"vue": "^3.5.17",
|
||||
"vue-3-sanitize": "^0.1.4",
|
||||
"vue-loader": "^17.4.2",
|
||||
"vue-loader-plugin": "^1.3.0",
|
||||
|
|
@ -130,21 +133,19 @@
|
|||
"vue-sanitize-directive": "^0.2.1",
|
||||
"vue-style-loader": "^4.1.3",
|
||||
"vue3-gettext": "^2.4.0",
|
||||
"vuetify": "^3.8.4",
|
||||
"webpack": "^5.99.8",
|
||||
"vuetify": "^3.9.0",
|
||||
"webpack": "^5.100.2",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-hot-middleware": "^2.26.1",
|
||||
"webpack-manifest-plugin": "^5.0.1",
|
||||
"webpack-md5-hash": "^0.0.6",
|
||||
"webpack-merge": "^6.0.1",
|
||||
"webpack-plugin-vuetify": "^3.1.1",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"@vue/test-utils": "^2.4.6"
|
||||
"webpack-plugin-vuetify": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0",
|
||||
"npm": ">= 9.0.0",
|
||||
"yarn": "please use npm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,10 +100,18 @@ export default [
|
|||
meta: {
|
||||
title: $gettext("Settings"),
|
||||
requiresAuth: true,
|
||||
admin: true,
|
||||
settings: true,
|
||||
background: "background",
|
||||
},
|
||||
beforeEnter: (to, from, next) => {
|
||||
if ($session.loginRequired()) {
|
||||
next({ name: loginRoute });
|
||||
} else if ($config.deny("users", "access_all")) {
|
||||
next({ name: $session.getDefaultRoute() });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "upgrade",
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export const FormatWebmAv1 = "webm_av1";
|
|||
export const FormatMkvAv1 = "mkv_av1";
|
||||
export const FormatTheora = "ogg";
|
||||
export const FormatWebp = "webp";
|
||||
export const FormatM2TS = "m2t";
|
||||
|
||||
// Image file formats:
|
||||
export const FormatJpeg = "jpg";
|
||||
|
|
@ -51,6 +52,7 @@ export const FormatSVG = "svg";
|
|||
// Content type strings for common media formats, see https://tools.woolyss.com/html5-canplaytype-tester/:
|
||||
export const ContentTypeMp4 = "video/mp4";
|
||||
export const ContentTypeMp4AvcMain = ContentTypeMp4 + '; codecs="avc1.4d0028"'; // AVC High Profile Level 4
|
||||
export const ContentTypeMp4AvcHigh = ContentTypeMp4 + '; codecs="avc1.640028"'; // MPEG-4 AVC (H.264), High Level 4.0
|
||||
export const ContentTypeMp4HvcMain = ContentTypeMp4 + '; codecs="hvc1.1.6.L93.B0"';
|
||||
export const ContentTypeMp4HvcMain10 = ContentTypeMp4 + '; codecs="hvc1.2.4.L153.B0"';
|
||||
export const ContentTypeMp4HevMain = ContentTypeMp4 + '; codecs="hev1.1.6.L93.B0"';
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export const Auto = "";
|
|||
export const Default = "default";
|
||||
export const Manual = "manual";
|
||||
export const Estimate = "estimate";
|
||||
export const File = "file";
|
||||
export const Name = "name";
|
||||
export const Meta = "meta";
|
||||
export const Xmp = "xmp";
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ export default class $util {
|
|||
}
|
||||
|
||||
static formatNs(d) {
|
||||
if (!d || typeof d !== "number") {
|
||||
if (!d || Number.isNaN(d)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
@ -423,8 +423,9 @@ export default class $util {
|
|||
return "Matroska Multimedia Container";
|
||||
case "mts":
|
||||
return "Advanced Video Coding High Definition (AVCHD)";
|
||||
case "m2t":
|
||||
case "m2ts":
|
||||
return "Blu-ray MPEG-2 Transport Stream";
|
||||
return "MPEG-2 Transport Stream (M2TS)";
|
||||
case "webp":
|
||||
return "Google WebP";
|
||||
case media.FormatWebm:
|
||||
|
|
@ -524,6 +525,8 @@ export default class $util {
|
|||
case media.CodecVp09:
|
||||
case media.FormatVp9:
|
||||
return "VP9";
|
||||
case media.FormatM2TS:
|
||||
return "M2TS";
|
||||
case "extended webp":
|
||||
case media.FormatWebp:
|
||||
return "WebP";
|
||||
|
|
@ -578,6 +581,9 @@ export default class $util {
|
|||
return "Extended WebP";
|
||||
case "webm":
|
||||
return "Google WebM";
|
||||
case "m2t":
|
||||
case "m2ts":
|
||||
return "MPEG-2 Transport Stream (M2TS)";
|
||||
case "mpeg":
|
||||
return "Moving Picture Experts Group (MPEG)";
|
||||
case "mjpg":
|
||||
|
|
@ -825,6 +831,10 @@ export default class $util {
|
|||
|
||||
static copyText(text) {
|
||||
if (!text) {
|
||||
if (debug) {
|
||||
console.warn("clipboard: missing text");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -847,15 +857,18 @@ export default class $util {
|
|||
})
|
||||
.catch((err) => {
|
||||
if (debug && err) {
|
||||
console.log("copy:", err);
|
||||
console.error("clipboard:", err);
|
||||
}
|
||||
|
||||
$notify.error($gettext("Not allowed"));
|
||||
$notify.error($gettext("Cannot copy to clipboard"));
|
||||
});
|
||||
return true;
|
||||
} else if (debug) {
|
||||
console.warn("clipboard: window.navigator.clipboard is not an instance of EventTarget");
|
||||
}
|
||||
|
||||
$notify.warn($gettext("Not supported"));
|
||||
$notify.warn($gettext("Cannot copy to clipboard"));
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,26 +18,28 @@
|
|||
</template>
|
||||
|
||||
<v-list slim nav density="compact" bg-color="navigation" class="action-menu__list">
|
||||
<v-list-item
|
||||
v-for="action in actions"
|
||||
:key="action.name"
|
||||
:value="action.name"
|
||||
:prepend-icon="action.icon"
|
||||
:title="action.text"
|
||||
:class="action.class ? action.class : 'action-' + action.name"
|
||||
:to="action.to ? action.to : undefined"
|
||||
:href="action.href ? action.href : undefined"
|
||||
:link="true"
|
||||
:target="action.target ? '_blank' : '_self'"
|
||||
:disabled="action.disabled"
|
||||
:nav="true"
|
||||
class="action-menu__item"
|
||||
@click="action.click"
|
||||
>
|
||||
<template v-if="action.shortcut && !$isMobile" #append>
|
||||
<div class="action-menu__shortcut">{{ action.shortcut }}</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<template v-for="action in actions" :key="action.name">
|
||||
<v-divider v-if="action?.color === 'danger'"></v-divider>
|
||||
<v-list-item
|
||||
:value="action.name"
|
||||
:prepend-icon="action.icon"
|
||||
:title="action.text"
|
||||
:base-color="action.color"
|
||||
:class="action.class ? action.class : 'action-' + action.name"
|
||||
:to="action.to ? action.to : undefined"
|
||||
:href="action.href ? action.href : undefined"
|
||||
:link="true"
|
||||
:target="action.target ? '_blank' : '_self'"
|
||||
:disabled="action.disabled"
|
||||
:nav="true"
|
||||
class="action-menu__item"
|
||||
@click="action.click"
|
||||
>
|
||||
<template v-if="action.shortcut && !$isMobile" #append>
|
||||
<div class="action-menu__shortcut">{{ action.shortcut }}</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["close", "confirm"],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -67,19 +67,30 @@
|
|||
@confirm="dialog.upload = false"
|
||||
></p-service-upload>
|
||||
<p-album-edit-dialog :visible="dialog.edit" :album="album" @close="dialog.edit = false"></p-album-edit-dialog>
|
||||
<p-confirm-dialog
|
||||
:visible="dialog.delete"
|
||||
:text="$gettext('Are you sure you want to delete this album?')"
|
||||
:action="$gettext('Delete')"
|
||||
icon="mdi-delete-outline"
|
||||
@close="dialog.delete = false"
|
||||
@confirm="onDeleteConfirm"
|
||||
></p-confirm-dialog>
|
||||
</v-form>
|
||||
</template>
|
||||
<script>
|
||||
import $notify from "common/notify";
|
||||
import download from "common/download";
|
||||
import { T } from "common/gettext";
|
||||
import $api from "common/api";
|
||||
|
||||
import PActionMenu from "component/action/menu.vue";
|
||||
import PConfirmDialog from "component/confirm/dialog.vue";
|
||||
|
||||
export default {
|
||||
name: "PAlbumToolbar",
|
||||
components: {
|
||||
PActionMenu,
|
||||
PConfirmDialog,
|
||||
},
|
||||
props: {
|
||||
album: {
|
||||
|
|
@ -117,6 +128,7 @@ export default {
|
|||
this.$config.allow("albums", "download") && features.download && !settings?.albums?.download?.disabled,
|
||||
canShare: this.$config.allow("albums", "share") && features.share,
|
||||
canManage: this.$config.allow("albums", "manage"),
|
||||
canDelete: this.$config.allow("albums", "delete"),
|
||||
experimental: this.$config.get("experimental"),
|
||||
isFullScreen: !!document.fullscreenElement,
|
||||
categories: this.$config.albumCategories(),
|
||||
|
|
@ -128,6 +140,7 @@ export default {
|
|||
share: false,
|
||||
upload: false,
|
||||
edit: false,
|
||||
delete: false,
|
||||
},
|
||||
titleRule: (v) => v.length <= this.$config.get("clip") || this.$gettext("Name too long"),
|
||||
};
|
||||
|
|
@ -193,6 +206,16 @@ export default {
|
|||
this.download();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete",
|
||||
color: "danger",
|
||||
icon: "mdi-delete-outline",
|
||||
text: this.$gettext("Delete Album"),
|
||||
visible: this.canDelete && ["album", "moment", "state"].includes(this.album.Type),
|
||||
click: () => {
|
||||
this.dialog.delete = true;
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
T() {
|
||||
|
|
@ -230,6 +253,12 @@ export default {
|
|||
|
||||
download(path, "album.zip");
|
||||
},
|
||||
onDeleteConfirm() {
|
||||
$api.delete(`albums/${this.album.UID}`).catch(() => {
|
||||
$notify.error(this.$gettext("Unable to delete"));
|
||||
});
|
||||
this.dialog.delete = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1878,7 +1878,7 @@ export default {
|
|||
},
|
||||
// Jumps to the specified time index when a video is loaded and seekable.
|
||||
seekVideo(seekTo) {
|
||||
if (typeof seekTo !== "number") {
|
||||
if (Number.isNaN(seekTo)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -1891,19 +1891,39 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
if (seekTo > video.duration) {
|
||||
video.currentTime = video.duration;
|
||||
} else if (seekTo <= 0) {
|
||||
video.currentTime = 0;
|
||||
// If possible, use the fastSeek() method to quickly jump to the new time index:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/fastSeek
|
||||
if (typeof video.fastSeek === "function") {
|
||||
if (seekTo >= video.duration - 0.01) {
|
||||
video.loop = false;
|
||||
video.fastSeek(video.duration);
|
||||
this.pauseVideo(video);
|
||||
} else if (seekTo <= 0) {
|
||||
video.loop = false;
|
||||
video.fastSeek(0);
|
||||
this.pauseVideo(video);
|
||||
} else {
|
||||
video.fastSeek(seekTo);
|
||||
}
|
||||
} else {
|
||||
video.currentTime = seekTo;
|
||||
if (seekTo >= video.duration - 0.01) {
|
||||
video.loop = false;
|
||||
video.currentTime = video.duration;
|
||||
this.pauseVideo(video);
|
||||
} else if (seekTo <= 0) {
|
||||
video.loop = false;
|
||||
video.currentTime = 0;
|
||||
this.pauseVideo(video);
|
||||
} else {
|
||||
video.currentTime = seekTo;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
// Skips the specified number of seconds when a video is loaded and seekable.
|
||||
seekVideoSeconds(seconds) {
|
||||
if (!seconds || typeof seconds !== "number") {
|
||||
if (!seconds || Number.isNaN(seconds)) {
|
||||
return false;
|
||||
} else if (!this.video.playing) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ export default {
|
|||
increase(amount) {
|
||||
let o = this.progress;
|
||||
|
||||
if (o < 100 && typeof amount !== "number") {
|
||||
if (o < 100 && Number.isNaN(amount)) {
|
||||
if (o >= 0 && o < 25) {
|
||||
amount = Math.random() * 3 + 3;
|
||||
} else if (o >= 25 && o < 50) {
|
||||
|
|
|
|||
341
frontend/src/component/location/dialog.vue
Normal file
341
frontend/src/component/location/dialog.vue
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
<template>
|
||||
<v-dialog
|
||||
:model-value="visible"
|
||||
:max-width="900"
|
||||
:fullscreen="$vuetify.display.xs"
|
||||
persistent
|
||||
scrim
|
||||
scrollable
|
||||
class="p-location-dialog"
|
||||
@keydown.esc="close"
|
||||
@after-enter="afterEnter"
|
||||
@after-leave="afterLeave"
|
||||
>
|
||||
<v-card :tile="$vuetify.display.xs">
|
||||
<v-toolbar v-if="$vuetify.display.xs" flat color="navigation" class="mb-4" density="compact">
|
||||
<v-btn icon @click.stop="close">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>
|
||||
{{ $gettext("Adjust Location") }}
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-title v-else class="d-flex justify-start align-center ga-3">
|
||||
<v-icon size="28" color="primary">mdi-map-marker</v-icon>
|
||||
<h6 class="text-h6">{{ $gettext("Adjust Location") }}</h6>
|
||||
</v-card-title>
|
||||
<v-card-text class="pb-3">
|
||||
<div class="d-flex flex-column flex-md-row ga-5">
|
||||
<div class="flex-grow-1 position-relative mb-4 mb-md-0">
|
||||
<p-map
|
||||
ref="map"
|
||||
:latlng="[currentLat, currentLng]"
|
||||
:zoom="12"
|
||||
:style="style"
|
||||
:interactive="true"
|
||||
:draggable="true"
|
||||
:show-controls="true"
|
||||
:clickable="true"
|
||||
@marker-moved="onMarkerMoved"
|
||||
@map-clicked="onMapClicked"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="map-sidebar d-flex flex-column"
|
||||
:class="$vuetify.display.xs ? `ga-3` : 'ga-5'"
|
||||
:style="{
|
||||
width: $vuetify.display.smAndDown ? '100%' : '300px',
|
||||
maxWidth: $vuetify.display.smAndDown ? '100%' : '300px',
|
||||
minWidth: 0,
|
||||
}"
|
||||
>
|
||||
<div>
|
||||
<v-autocomplete
|
||||
ref="search"
|
||||
v-model="selectedPlace"
|
||||
:items="searchResults"
|
||||
:loading="searchLoading"
|
||||
:search="searchQuery"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
:placeholder="$gettext(`Search`)"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
return-object
|
||||
auto-select-first
|
||||
clearable
|
||||
autocomplete="off"
|
||||
no-filter
|
||||
menu-icon=""
|
||||
:menu-props="{ maxHeight: 300 }"
|
||||
@update:search="onSearchQueryChange"
|
||||
@update:model-value="onPlaceSelected"
|
||||
@click:clear="clearSearch"
|
||||
>
|
||||
<template #item="{ props }">
|
||||
<v-list-item v-bind="props" density="compact">
|
||||
<template #prepend>
|
||||
<v-icon>mdi-map-marker</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template #no-data>
|
||||
<v-list-item
|
||||
v-if="searchQuery && searchQuery.length >= 2 && !searchLoading && searchResults.length === 0"
|
||||
>
|
||||
<v-list-item-title>{{ $gettext("No results") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</div>
|
||||
<!-- div v-if="locationInfo">
|
||||
<div class="text-subtitle-2 mb-2">{{ $gettext("Location Details") }}</div>
|
||||
<div class="text-body-2">
|
||||
{{ simplifiedLocationDisplay }}
|
||||
</div>
|
||||
</div -->
|
||||
|
||||
<div class="text-body-2 mt-3">
|
||||
{{ $gettext("You can search for a location or move the marker on the map to change the position:") }}
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<p-location-input
|
||||
:latlng="[currentLat, currentLng]"
|
||||
density="comfortable"
|
||||
:enable-undo="true"
|
||||
:auto-apply="true"
|
||||
:label="locationLabel"
|
||||
@update:latlng="onLatLngUpdate"
|
||||
@changed="onLocationChanged"
|
||||
@cleared="onLocationCleared"
|
||||
></p-location-input>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<v-btn variant="flat" color="button" class="action-cancel" min-width="120" @click.stop="close">
|
||||
{{ $gettext("Cancel") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="highlight"
|
||||
min-width="120"
|
||||
:disabled="!(currentLat !== null && currentLng !== null) || locationLoading"
|
||||
:loading="locationLoading"
|
||||
@click="confirm"
|
||||
>
|
||||
{{ $gettext("Confirm") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PLocationInput from "component/location/input.vue";
|
||||
import PMap from "component/map.vue";
|
||||
|
||||
export default {
|
||||
name: "PLocationDialog",
|
||||
components: {
|
||||
PLocationInput,
|
||||
PMap,
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
latlng: {
|
||||
type: Array,
|
||||
default: () => [0, 0],
|
||||
},
|
||||
style: {
|
||||
type: String,
|
||||
default: "embedded",
|
||||
},
|
||||
},
|
||||
emits: ["update:latlng", "close", "confirm"],
|
||||
data() {
|
||||
return {
|
||||
currentLat: this.latlng[0],
|
||||
currentLng: this.latlng[1],
|
||||
location: null,
|
||||
locationLoading: false,
|
||||
searchQuery: "",
|
||||
searchResults: [],
|
||||
searchLoading: false,
|
||||
searchTimeout: null,
|
||||
selectedPlace: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
locationLabel() {
|
||||
if (!this.location || !this.location?.place?.label) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return this.location.place.label;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(show) {
|
||||
if (show) {
|
||||
this.currentLat = this.latlng[0];
|
||||
this.currentLng = this.latlng[1];
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit("close");
|
||||
},
|
||||
confirm() {
|
||||
if (this.currentLat !== null && this.currentLng !== null) {
|
||||
this.$emit("update:latlng", [this.currentLat, this.currentLng]);
|
||||
this.$emit("confirm", {
|
||||
lat: this.currentLat,
|
||||
lng: this.currentLng,
|
||||
location: this.location,
|
||||
});
|
||||
}
|
||||
},
|
||||
afterEnter() {
|
||||
if (this.currentLat && this.currentLng && !(this.currentLat === 0 && this.currentLng === 0)) {
|
||||
this.fetchLocationInfo(this.currentLat, this.currentLng);
|
||||
}
|
||||
},
|
||||
afterLeave() {
|
||||
this.location = null;
|
||||
this.locationLoading = false;
|
||||
this.resetSearchState();
|
||||
},
|
||||
onMarkerMoved(event) {
|
||||
this.setPositionAndFetchInfo(event.lat, event.lng);
|
||||
},
|
||||
onMapClicked(event) {
|
||||
this.setPositionAndFetchInfo(event.lat, event.lng);
|
||||
},
|
||||
onLocationChanged(data) {
|
||||
if (data.lat && data.lng && !(data.lat === 0 && data.lng === 0)) {
|
||||
this.fetchLocationInfo(data.lat, data.lng);
|
||||
}
|
||||
},
|
||||
onLatLngUpdate(latlng) {
|
||||
this.currentLat = latlng[0];
|
||||
this.currentLng = latlng[1];
|
||||
},
|
||||
onLocationCleared() {
|
||||
this.location = null;
|
||||
this.locationLoading = false;
|
||||
|
||||
// Use the map component's removeMarker method
|
||||
if (this.$refs.map) {
|
||||
this.$refs.map.removeMarker();
|
||||
this.$refs.map.flyTo(20, 0, 2); // lat, lng, zoom
|
||||
}
|
||||
},
|
||||
clearSearchTimeout() {
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
this.searchTimeout = null;
|
||||
}
|
||||
},
|
||||
resetSearchState() {
|
||||
this.searchQuery = "";
|
||||
this.searchResults = [];
|
||||
this.selectedPlace = null;
|
||||
this.searchLoading = false;
|
||||
this.clearSearchTimeout();
|
||||
},
|
||||
setPositionAndFetchInfo(lat, lng) {
|
||||
this.currentLat = lat;
|
||||
this.currentLng = lng;
|
||||
this.fetchLocationInfo(lat, lng);
|
||||
},
|
||||
fetchLocationInfo(lat, lng) {
|
||||
this.locationLoading = true;
|
||||
this.$api
|
||||
.get(`places/reverse?lat=${lat}&lng=${lng}`)
|
||||
.then((response) => {
|
||||
if (response.data && response.data?.place?.label) {
|
||||
this.location = response.data;
|
||||
} else {
|
||||
this.location = null;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Reverse geocoding error:", error);
|
||||
this.location = null;
|
||||
})
|
||||
.finally(() => {
|
||||
this.locationLoading = false;
|
||||
});
|
||||
},
|
||||
onSearchQueryChange(query) {
|
||||
this.searchQuery = query;
|
||||
this.clearSearchTimeout();
|
||||
|
||||
if (!query || query.length < 2) {
|
||||
this.searchResults = [];
|
||||
this.searchLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.searchLoading = true;
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.performPlaceSearch(query);
|
||||
}, 300); // 300ms delay after user stops typing
|
||||
},
|
||||
async performPlaceSearch(query) {
|
||||
if (!query || query.length < 2) {
|
||||
this.searchLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.$api.get("places/search", {
|
||||
params: {
|
||||
q: query,
|
||||
count: 10,
|
||||
locale: this.$config.getLanguageLocale() || "en",
|
||||
},
|
||||
});
|
||||
|
||||
if (this.searchQuery === query) {
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
this.searchResults = response.data;
|
||||
} else {
|
||||
this.searchResults = [];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Place search error:", error);
|
||||
if (this.searchQuery === query) {
|
||||
this.searchResults = [];
|
||||
}
|
||||
} finally {
|
||||
if (this.searchQuery === query) {
|
||||
this.searchLoading = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
onPlaceSelected(place) {
|
||||
if (place && place.lat && place.lng) {
|
||||
this.setPositionAndFetchInfo(place.lat, place.lng);
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.resetSearchState();
|
||||
});
|
||||
}
|
||||
},
|
||||
clearSearch() {
|
||||
this.resetSearchState();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
252
frontend/src/component/location/input.vue
Normal file
252
frontend/src/component/location/input.vue
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
<template>
|
||||
<v-text-field
|
||||
v-model="coordinateInput"
|
||||
:disabled="disabled"
|
||||
:hide-details="hideDetails"
|
||||
:label="label"
|
||||
:placeholder="placeholder"
|
||||
:density="density"
|
||||
:validate-on="validateOn"
|
||||
:rules="[() => !coordinateInput || isValidCoordinateInput]"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
class="input-coordinates"
|
||||
@keydown.enter="applyCoordinates"
|
||||
@update:model-value="onCoordinateInputChange"
|
||||
@paste="pastePosition"
|
||||
>
|
||||
<template v-if="icon" #prepend-inner>
|
||||
<v-icon
|
||||
v-if="showMapButton"
|
||||
variant="plain"
|
||||
:icon="icon"
|
||||
:title="mapButtonTitle"
|
||||
:disabled="mapButtonDisabled"
|
||||
class="action-map"
|
||||
@click.stop="$emit('open-map')"
|
||||
>
|
||||
</v-icon>
|
||||
<v-icon v-else variant="plain" :icon="icon" class="text-disabled"> </v-icon>
|
||||
</template>
|
||||
<template #append-inner>
|
||||
<v-icon
|
||||
v-if="showUndoButton"
|
||||
variant="plain"
|
||||
icon="mdi-undo"
|
||||
class="action-undo"
|
||||
@click.stop="undoClear"
|
||||
></v-icon>
|
||||
<v-icon
|
||||
v-else-if="coordinateInput"
|
||||
variant="plain"
|
||||
icon="mdi-close-circle"
|
||||
class="action-clear"
|
||||
@click.stop="clearCoordinates"
|
||||
></v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "PLocationInput",
|
||||
props: {
|
||||
latlng: {
|
||||
type: Array,
|
||||
default: () => [null, null],
|
||||
validator: (value) => Array.isArray(value) && value.length === 2,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideDetails: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "37.75267, -122.543",
|
||||
},
|
||||
density: {
|
||||
type: String,
|
||||
default: "comfortable",
|
||||
},
|
||||
validateOn: {
|
||||
type: String,
|
||||
default: "input",
|
||||
},
|
||||
showMapButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "mdi-map-marker",
|
||||
},
|
||||
mapButtonTitle: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
mapButtonDisabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
enableUndo: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
autoApply: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
debounceDelay: {
|
||||
type: Number,
|
||||
default: 1000,
|
||||
},
|
||||
},
|
||||
emits: ["update:latlng", "changed", "cleared", "open-map"],
|
||||
data() {
|
||||
return {
|
||||
coordinateInput: "",
|
||||
inputTimeout: null,
|
||||
wasCleared: false,
|
||||
lastValidLat: null,
|
||||
lastValidLng: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isValidCoordinateInput() {
|
||||
if (!this.coordinateInput) return false;
|
||||
|
||||
const parts = this.coordinateInput.split(",").map((part) => part.trim());
|
||||
if (parts.length !== 2) return false;
|
||||
|
||||
const lat = parseFloat(parts[0]);
|
||||
const lng = parseFloat(parts[1]);
|
||||
|
||||
return !isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180;
|
||||
},
|
||||
showUndoButton() {
|
||||
return this.enableUndo && this.wasCleared && this.lastValidLat !== null && this.lastValidLng !== null;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
latlng() {
|
||||
this.updateCoordinateInput();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.updateCoordinateInput();
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.inputTimeout) {
|
||||
clearTimeout(this.inputTimeout);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateCoordinateInput() {
|
||||
const lat = this.latlng[0];
|
||||
const lng = this.latlng[1];
|
||||
|
||||
if (lat !== null && lng !== null && !(lat === 0 && lng === 0) && !isNaN(lat) && !isNaN(lng)) {
|
||||
this.coordinateInput = `${parseFloat(lat)}, ${parseFloat(lng)}`;
|
||||
this.wasCleared = false;
|
||||
} else {
|
||||
this.coordinateInput = "";
|
||||
}
|
||||
},
|
||||
|
||||
onCoordinateInputChange(value) {
|
||||
this.coordinateInput = value;
|
||||
this.wasCleared = false;
|
||||
|
||||
if (this.inputTimeout) {
|
||||
clearTimeout(this.inputTimeout);
|
||||
}
|
||||
|
||||
if (this.autoApply) {
|
||||
this.inputTimeout = setTimeout(() => {
|
||||
if (this.isValidCoordinateInput) {
|
||||
this.applyCoordinates();
|
||||
}
|
||||
}, this.debounceDelay);
|
||||
}
|
||||
},
|
||||
applyCoordinates() {
|
||||
if (!this.isValidCoordinateInput) return;
|
||||
|
||||
const parts = this.coordinateInput.split(",").map((part) => part.trim());
|
||||
const lat = parseFloat(parts[0]);
|
||||
const lng = parseFloat(parts[1]);
|
||||
|
||||
this.$emit("update:latlng", [lat, lng]);
|
||||
this.$emit("changed", { lat: lat, lng: lng });
|
||||
},
|
||||
clearCoordinates() {
|
||||
if (this.enableUndo) {
|
||||
this.lastValidLat = this.latlng[0];
|
||||
this.lastValidLng = this.latlng[1];
|
||||
}
|
||||
|
||||
this.coordinateInput = "";
|
||||
this.wasCleared = true;
|
||||
|
||||
this.$emit("update:latlng", [0, 0]);
|
||||
this.$emit("changed", { lat: 0, lng: 0 });
|
||||
this.$emit("cleared", {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
previousLatitude: this.lastValidLat,
|
||||
previousLongitude: this.lastValidLng,
|
||||
});
|
||||
},
|
||||
undoClear() {
|
||||
if (this.lastValidLat !== null && this.lastValidLng !== null) {
|
||||
this.$emit("update:latlng", [this.lastValidLat, this.lastValidLng]);
|
||||
this.$emit("changed", {
|
||||
lat: this.lastValidLat,
|
||||
lng: this.lastValidLng,
|
||||
});
|
||||
|
||||
this.wasCleared = false;
|
||||
this.lastValidLat = null;
|
||||
this.lastValidLng = null;
|
||||
}
|
||||
},
|
||||
pastePosition(event) {
|
||||
// Autofill the lat and lng fields if the text in the clipboard contains two float values.
|
||||
const clipboard = event.clipboardData ? event.clipboardData : window.clipboardData;
|
||||
|
||||
if (!clipboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get values from browser clipboard.
|
||||
const text = clipboard.getData("text");
|
||||
|
||||
// Trim spaces before splitting by whitespace and/or commas.
|
||||
const val = text.trim().split(/[ ,]+/);
|
||||
|
||||
if (val.length >= 2) {
|
||||
const lat = parseFloat(val[0]);
|
||||
const lng = parseFloat(val[1]);
|
||||
|
||||
if (!isNaN(lat) && lat >= -90 && lat <= 90 && !isNaN(lng) && lng >= -180 && lng <= 180) {
|
||||
// Update coordinates
|
||||
this.$emit("update:latlng", [lat, lng]);
|
||||
this.$emit("changed", { lat: lat, lng: lng });
|
||||
|
||||
// Prevent default action.
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -10,13 +10,10 @@ let maplibregl = null;
|
|||
export default {
|
||||
name: "PMap",
|
||||
props: {
|
||||
lat: {
|
||||
type: Number,
|
||||
default: 0.0,
|
||||
},
|
||||
lng: {
|
||||
type: Number,
|
||||
default: 0.0,
|
||||
latlng: {
|
||||
type: Array,
|
||||
default: () => [0.0, 0.0],
|
||||
validator: (value) => Array.isArray(value) && value.length === 2,
|
||||
},
|
||||
zoom: {
|
||||
type: Number,
|
||||
|
|
@ -26,12 +23,33 @@ export default {
|
|||
type: String,
|
||||
default: "embedded",
|
||||
},
|
||||
// Interactive mode props
|
||||
interactive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showControls: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
clickable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["update:latlng", "marker-moved", "map-clicked"],
|
||||
data() {
|
||||
const settings = this.$config.getSettings();
|
||||
|
||||
return {
|
||||
map: null,
|
||||
marker: null,
|
||||
position: [0.0, 0.0],
|
||||
animate: settings.maps.animate,
|
||||
options: {
|
||||
container: null,
|
||||
// Styles can be edited/created with https://maplibre.org/maputnik/.
|
||||
|
|
@ -41,17 +59,14 @@ export default {
|
|||
style: `https://cdn.photoprism.app/maps/${this.style}.json`,
|
||||
glyphs: `https://cdn.photoprism.app/maps/font/{fontstack}/{range}.pbf`,
|
||||
zoom: this.zoom,
|
||||
interactive: true,
|
||||
interactive: this.interactive,
|
||||
attributionControl: false,
|
||||
},
|
||||
loaded: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
lat() {
|
||||
this.updatePosition();
|
||||
},
|
||||
lng() {
|
||||
latlng() {
|
||||
this.updatePosition();
|
||||
},
|
||||
},
|
||||
|
|
@ -74,52 +89,154 @@ export default {
|
|||
|
||||
try {
|
||||
this.options.container = this.$refs.map;
|
||||
|
||||
// Set center based on coordinates or default
|
||||
if (!(this.latlng[0] && this.latlng[1] && !(this.latlng[0] === 0 && this.latlng[1] === 0))) {
|
||||
this.options.zoom = 2;
|
||||
this.options.center = [0, 20];
|
||||
} else {
|
||||
this.options.center = [this.latlng[1], this.latlng[0]]; // Convert [lat, lng] to [lng, lat] for MapLibre
|
||||
}
|
||||
|
||||
this.map = new maplibregl.Map(this.options);
|
||||
|
||||
// Add controls.
|
||||
/* this.map.addControl(
|
||||
new maplibregl.NavigationControl({
|
||||
showCompass: false,
|
||||
showZoom: true,
|
||||
visualizePitch: false,
|
||||
}),
|
||||
"top-right"
|
||||
);
|
||||
// Add controls if requested
|
||||
if (this.showControls) {
|
||||
this.map.addControl(
|
||||
new maplibregl.NavigationControl({
|
||||
showCompass: true,
|
||||
showZoom: true,
|
||||
visualizePitch: false,
|
||||
}),
|
||||
"top-right"
|
||||
);
|
||||
|
||||
this.map.addControl(new maplibregl.ScaleControl({ maxWidth: 80, unit: "metric" }), "bottom-left");
|
||||
*/
|
||||
this.map.addControl(new maplibregl.ScaleControl({ maxWidth: 80, unit: "metric" }), "bottom-left");
|
||||
|
||||
this.map.addControl(
|
||||
new maplibregl.GeolocateControl({
|
||||
positionOptions: {
|
||||
enableHighAccuracy: true,
|
||||
},
|
||||
trackUserLocation: true,
|
||||
}),
|
||||
"top-right"
|
||||
);
|
||||
}
|
||||
|
||||
this.map.on("error", (e) => {
|
||||
console.error("map:", e);
|
||||
});
|
||||
|
||||
// Handle missing style images
|
||||
this.map.on("styleimagemissing", (e) => {
|
||||
const emptyImage = new ImageData(1, 1);
|
||||
if (e && e.id) {
|
||||
this.map.addImage(e.id, emptyImage);
|
||||
}
|
||||
});
|
||||
|
||||
this.map.on("load", () => {
|
||||
this.loaded = true;
|
||||
this.updatePosition();
|
||||
this.map.resize();
|
||||
});
|
||||
|
||||
// Add click handler for interactive mode
|
||||
if (this.clickable) {
|
||||
this.map.on("click", (e) => {
|
||||
const lat = e.lngLat.lat;
|
||||
const lng = e.lngLat.lng;
|
||||
this.$emit("map-clicked", { lat, lng });
|
||||
this.$emit("update:latlng", [lat, lng]);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("map: initialization failed", error);
|
||||
this.loaded = false;
|
||||
}
|
||||
},
|
||||
updatePosition() {
|
||||
if (this.map && this.loaded) {
|
||||
if (this.position[0] === this.lng && this.position[1] === this.lat) {
|
||||
return;
|
||||
}
|
||||
if (!this.map || !this.loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.position = [this.lng, this.lat];
|
||||
this.map.setCenter(this.position);
|
||||
if (this.position[0] === this.latlng[1] && this.position[1] === this.latlng[0] && this.marker) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip invalid or empty coordinates
|
||||
if (!(this.latlng[0] && this.latlng[1] && !(this.latlng[0] === 0 && this.latlng[1] === 0))) {
|
||||
if (this.marker) {
|
||||
this.marker.setLngLat(this.position);
|
||||
this.marker.remove();
|
||||
this.marker = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.position = [this.latlng[1], this.latlng[0]]; // Convert [lat, lng] to [lng, lat] for MapLibre
|
||||
|
||||
if (this.animate > 0) {
|
||||
this.map.flyTo({
|
||||
center: this.position,
|
||||
zoom: this.interactive ? this.zoom : undefined, // Only set zoom in interactive mode
|
||||
duration: this.animate,
|
||||
essential: true, // Respects prefers-reduced-motion
|
||||
});
|
||||
} else {
|
||||
// Use setCenter for instant positioning (no animation)
|
||||
if (this.interactive) {
|
||||
this.map.setCenter(this.position, {
|
||||
zoom: this.zoom,
|
||||
animate: false,
|
||||
});
|
||||
} else {
|
||||
this.marker = new maplibregl.Marker({
|
||||
color: "#3fb4df",
|
||||
draggable: false,
|
||||
})
|
||||
.setLngLat(this.position)
|
||||
.addTo(this.map);
|
||||
this.map.setCenter(this.position);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.marker) {
|
||||
this.marker.setLngLat(this.position);
|
||||
} else {
|
||||
this.marker = new maplibregl.Marker({
|
||||
color: "#3fb4df",
|
||||
draggable: this.draggable,
|
||||
})
|
||||
.setLngLat(this.position)
|
||||
.addTo(this.map);
|
||||
|
||||
// Add drag event listener for draggable markers
|
||||
if (this.draggable) {
|
||||
this.marker.on("dragend", () => {
|
||||
const lngLat = this.marker.getLngLat();
|
||||
this.$emit("marker-moved", { lat: lngLat.lat, lng: lngLat.lng });
|
||||
this.$emit("update:latlng", [lngLat.lat, lngLat.lng]);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
// Public method to remove marker
|
||||
removeMarker() {
|
||||
if (this.marker) {
|
||||
this.marker.remove();
|
||||
this.marker = null;
|
||||
}
|
||||
},
|
||||
// Public method to fly to coordinates
|
||||
flyTo(lat, lng, zoom = this.zoom) {
|
||||
if (this.map) {
|
||||
if (this.animate > 0) {
|
||||
this.map.flyTo({
|
||||
center: [lng, lat],
|
||||
zoom: zoom,
|
||||
duration: this.animate,
|
||||
essential: true,
|
||||
});
|
||||
} else {
|
||||
this.map.jumpTo({
|
||||
center: [lng, lat],
|
||||
zoom: zoom,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -351,11 +351,11 @@
|
|||
v-show="config.count.animated > 0"
|
||||
:to="{ name: 'animated' }"
|
||||
variant="text"
|
||||
class="nav-animated"
|
||||
class="nav-animated nav-animations"
|
||||
@click.stop=""
|
||||
>
|
||||
<v-list-item-title :class="`nav-menu-item menu-item`">
|
||||
{{ $gettext(`Animated`) }}
|
||||
{{ $gettext(`Animations`) }}
|
||||
</v-list-item-title>
|
||||
<span v-show="config.count.animated > 0" class="nav-count-item">{{ config.count.animated }}</span>
|
||||
</v-list-item>
|
||||
|
|
@ -1007,7 +1007,7 @@ export default {
|
|||
canAccessPrivate: !isRestricted && this.$config.allow("photos", "access_private"),
|
||||
canManagePhotos: canManagePhotos,
|
||||
canManagePeople: this.$config.allow("people", "manage"),
|
||||
canManageUsers: (!isPublic || isDemo) && this.$config.allow("users", "manage"),
|
||||
canManageUsers: (!isPublic || isDemo) && this.$config.allow("users", "access_all"),
|
||||
appNameSuffix: appNameSuffix,
|
||||
appName: this.$config.getName(),
|
||||
appAbout: this.$config.getAbout(),
|
||||
|
|
|
|||
|
|
@ -146,20 +146,36 @@
|
|||
@update:model-value="syncTime"
|
||||
></v-autocomplete>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="8" md="4">
|
||||
<v-col cols="12" sm="6" md="6">
|
||||
<p-location-input
|
||||
:latlng="[view.model.Lat, view.model.Lng]"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
:label="locationLabel"
|
||||
density="comfortable"
|
||||
validate-on="input"
|
||||
:show-map-button="!placesDisabled"
|
||||
:map-button-title="$gettext('Adjust Location')"
|
||||
:map-button-disabled="placesDisabled"
|
||||
class="input-coordinates"
|
||||
@update:latlng="updateLatLng"
|
||||
@changed="onLocationChanged"
|
||||
@open-map="adjustLocation"
|
||||
></p-location-input>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-autocomplete
|
||||
v-model="view.model.Country"
|
||||
:append-inner-icon="view.model.PlaceSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
:readonly="!!(view.model.Lat || view.model.Lng)"
|
||||
:placeholder="$gettext('Country')"
|
||||
:label="$gettext('Country')"
|
||||
hide-details
|
||||
hide-no-data
|
||||
autocomplete="off"
|
||||
item-value="Code"
|
||||
item-title="Name"
|
||||
:items="countries"
|
||||
prepend-inner-icon="mdi-map-marker"
|
||||
density="comfortable"
|
||||
validate-on="input"
|
||||
:rules="rules.country(true)"
|
||||
|
|
@ -167,7 +183,7 @@
|
|||
>
|
||||
</v-autocomplete>
|
||||
</v-col>
|
||||
<v-col cols="4" md="2">
|
||||
<v-col cols="2" class="hidden-sm-and-down">
|
||||
<v-text-field
|
||||
v-model="view.model.Altitude"
|
||||
:disabled="disabled"
|
||||
|
|
@ -183,42 +199,7 @@
|
|||
validate-on="input"
|
||||
:rules="rules.number(false, -10000, 1000000)"
|
||||
class="input-altitude"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="4" sm="6" md="3">
|
||||
<v-text-field
|
||||
v-model="view.model.Lat"
|
||||
:append-inner-icon="view.model.PlaceSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Latitude')"
|
||||
placeholder=""
|
||||
density="comfortable"
|
||||
validate-on="input"
|
||||
:rules="rules.lat(false)"
|
||||
class="input-latitude"
|
||||
@paste="pastePosition"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="4" sm="6" md="3">
|
||||
<v-text-field
|
||||
v-model="view.model.Lng"
|
||||
:append-inner-icon="view.model.PlaceSrc === 'manual' ? 'mdi-check' : ''"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:label="$gettext('Longitude')"
|
||||
placeholder=""
|
||||
density="comfortable"
|
||||
validate-on="input"
|
||||
:rules="rules.lng(false)"
|
||||
class="input-longitude"
|
||||
@paste="pastePosition"
|
||||
style="flex: 0 0 120px"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" class="p-camera-select">
|
||||
|
|
@ -432,6 +413,12 @@
|
|||
</div>
|
||||
</div>
|
||||
</v-form>
|
||||
<p-location-dialog
|
||||
:visible="locationDialog"
|
||||
:latlng="[view.model.Lat ? Number(view.model.Lat) : 0, view.model.Lng ? Number(view.model.Lng) : 0]"
|
||||
@close="locationDialog = false"
|
||||
@confirm="confirmLocation"
|
||||
></p-location-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -440,9 +427,15 @@ import countries from "options/countries.json";
|
|||
import Thumb from "model/thumb";
|
||||
import * as options from "options/options";
|
||||
import { rules } from "common/form";
|
||||
import PLocationDialog from "component/location/dialog.vue";
|
||||
import PLocationInput from "component/location/input.vue";
|
||||
|
||||
export default {
|
||||
name: "PTabPhotoDetails",
|
||||
components: {
|
||||
PLocationDialog,
|
||||
PLocationInput,
|
||||
},
|
||||
props: {
|
||||
uid: {
|
||||
type: String,
|
||||
|
|
@ -467,8 +460,11 @@ export default {
|
|||
showTimePicker: false,
|
||||
invalidDate: false,
|
||||
time: "",
|
||||
locationLabel: this.$gettext("Location"),
|
||||
locationDialog: false,
|
||||
textRule: (v) => v.length <= this.$config.get("clip") || this.$gettext("Text too long"),
|
||||
rtl: this.$isRtl,
|
||||
placesDisabled: !this.$config.feature("places"),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -484,11 +480,11 @@ export default {
|
|||
},
|
||||
watch: {
|
||||
uid() {
|
||||
this.syncTime();
|
||||
this.syncData();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.syncTime();
|
||||
this.syncData();
|
||||
},
|
||||
methods: {
|
||||
setDay(v) {
|
||||
|
|
@ -529,6 +525,22 @@ export default {
|
|||
this.updateModel();
|
||||
}
|
||||
},
|
||||
syncData() {
|
||||
this.syncLocation();
|
||||
this.syncTime();
|
||||
},
|
||||
syncLocation() {
|
||||
if (
|
||||
this.view?.model?.hasId() &&
|
||||
this.view?.model?.Place?.PlaceID &&
|
||||
this.view?.model?.Place?.PlaceID !== "zz" &&
|
||||
this.view?.model?.Place?.Label
|
||||
) {
|
||||
this.locationLabel = this.view.model.Place.Label;
|
||||
} else {
|
||||
this.locationLabel = this.$gettext("Location");
|
||||
}
|
||||
},
|
||||
syncTime() {
|
||||
if (!this.view?.model.hasId()) {
|
||||
return;
|
||||
|
|
@ -537,36 +549,6 @@ export default {
|
|||
const taken = this.view.model.getDateTime();
|
||||
this.time = taken.toFormat("HH:mm:ss");
|
||||
},
|
||||
pastePosition(event) {
|
||||
// Autofill the lat and lng fields if the text in the clipboard contains two float values.
|
||||
const clipboard = event.clipboardData ? event.clipboardData : window.clipboardData;
|
||||
|
||||
if (!clipboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get values from browser clipboard.
|
||||
const text = clipboard.getData("text");
|
||||
|
||||
// Trim spaces before splitting by whitespace and/or commas.
|
||||
const val = text.trim().split(/[ ,]+/);
|
||||
|
||||
// Two values found?
|
||||
if (val.length >= 2) {
|
||||
// Parse values.
|
||||
const lat = parseFloat(val[0]);
|
||||
const lng = parseFloat(val[1]);
|
||||
|
||||
// Lat and long must be valid floating point numbers.
|
||||
if (!isNaN(lat) && lat >= -90 && lat <= 90 && !isNaN(lng) && lng >= -180 && lng <= 180) {
|
||||
// Update view.model values.
|
||||
this.view.model.Lat = lat;
|
||||
this.view.model.Lng = lng;
|
||||
// Prevent default action.
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
updateModel() {
|
||||
if (!this.view?.model.hasId()) {
|
||||
return;
|
||||
|
|
@ -620,12 +602,39 @@ export default {
|
|||
this.$emit("close");
|
||||
}
|
||||
|
||||
this.syncTime();
|
||||
this.syncData();
|
||||
});
|
||||
},
|
||||
close() {
|
||||
this.$emit("close");
|
||||
},
|
||||
adjustLocation() {
|
||||
this.locationDialog = true;
|
||||
},
|
||||
confirmLocation(data) {
|
||||
if (data && data.lat !== undefined && data.lng !== undefined) {
|
||||
this.updateLatLng([data.lat, data.lng]);
|
||||
this.onLocationChanged(data);
|
||||
}
|
||||
|
||||
this.locationDialog = false;
|
||||
},
|
||||
updateLatLng(latlng) {
|
||||
this.view.model.Lat = latlng[0];
|
||||
this.view.model.Lng = latlng[1];
|
||||
this.view.model.PlaceSrc = "manual";
|
||||
},
|
||||
onLocationChanged(data) {
|
||||
if (data?.location?.country) {
|
||||
this.view.model.Country = data.location.country;
|
||||
}
|
||||
|
||||
if (data?.location?.place?.label) {
|
||||
this.locationLabel = data.location.place.label;
|
||||
} else {
|
||||
this.locationLabel = this.$gettext("Location");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -378,6 +378,8 @@ export default {
|
|||
return "XMP";
|
||||
case "estimate":
|
||||
return this.$gettext("Estimate");
|
||||
case "file":
|
||||
return this.$gettext("File");
|
||||
case "name":
|
||||
return this.$gettext("Name");
|
||||
case "title":
|
||||
|
|
|
|||
|
|
@ -357,7 +357,11 @@ export default {
|
|||
this.$view.leave(this);
|
||||
},
|
||||
onCopyAppPassword() {
|
||||
// Use the browser API to copy the app password to the clipboard.
|
||||
this.$util.copyText(this.appPassword);
|
||||
|
||||
// Flag the password as copied to the clipboard even
|
||||
// if the copyText() function returns an error.
|
||||
this.appPasswordCopied = true;
|
||||
},
|
||||
formatDate(d) {
|
||||
|
|
|
|||
|
|
@ -273,6 +273,7 @@ export default {
|
|||
default: () => this.$session.getUser(),
|
||||
},
|
||||
},
|
||||
emits: ["updateUser", "close"],
|
||||
data() {
|
||||
return {
|
||||
busy: false,
|
||||
|
|
@ -378,9 +379,12 @@ export default {
|
|||
});
|
||||
},
|
||||
onCopyRecoveryCode() {
|
||||
if (this.$util.copyText(this.key.RecoveryCode)) {
|
||||
this.recoveryCodeCopied = true;
|
||||
}
|
||||
// Use the browser API to copy the recovery code to the clipboard.
|
||||
this.$util.copyText(this.key.RecoveryCode);
|
||||
|
||||
// Flag the code as copied to the clipboard even
|
||||
// if the copyText() function returns an error.
|
||||
this.recoveryCodeCopied = true;
|
||||
},
|
||||
onActivate() {
|
||||
if (this.busy) {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="featPlaces" class="mx-0 px-0">
|
||||
<p-map :lat="model.Lat" :lng="model.Lng"></p-map>
|
||||
<p-map :latlng="[model.Lat, model.Lng]"></p-map>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
|
|
@ -120,10 +120,14 @@ export default {
|
|||
return this.$gettext("Unknown");
|
||||
}
|
||||
|
||||
if (model.TimeZone && model.TimeZone !== "Local") {
|
||||
return DateTime.fromISO(model.TakenAtLocal, { zone: model.TimeZone }).toLocaleString(formats.DATETIME_MED_TZ);
|
||||
// Always parse as UTC to avoid time shifts
|
||||
const dateTime = DateTime.fromISO(model.TakenAtLocal, { zone: "UTC" });
|
||||
|
||||
if (model.TimeZone && model.TimeZone !== "Local" && model.TimeZone !== "UTC") {
|
||||
// We use the real timezone just for display, but don't shift the time (prevents double timezone offset as backend already applied it)
|
||||
return dateTime.setZone(model.TimeZone, { keepLocalTime: true }).toLocaleString(formats.DATETIME_MED_TZ);
|
||||
} else {
|
||||
return DateTime.fromISO(model.TakenAtLocal, { zone: "UTC" }).toLocaleString(formats.DATETIME_MED);
|
||||
return dateTime.toLocaleString(formats.DATETIME_MED);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -45,6 +45,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Overflow */
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Layout Widths */
|
||||
|
||||
.width-sm, .width-md, .width-lg {
|
||||
|
|
|
|||
|
|
@ -126,16 +126,6 @@
|
|||
hyphens: auto;
|
||||
}
|
||||
|
||||
.p-lightbox__container > .p-lightbox__sidebar .p-map {
|
||||
display: block;
|
||||
min-height: 259px;
|
||||
aspect-ratio: 1;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Media Content */
|
||||
|
||||
.pswp__content {
|
||||
|
|
|
|||
|
|
@ -224,6 +224,28 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Embedded Map and Control Styles */
|
||||
|
||||
.p-map {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.p-location-dialog .p-map {
|
||||
height: 45vh;
|
||||
min-height: 250px;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.p-lightbox__container > .p-lightbox__sidebar .p-map {
|
||||
min-height: 259px;
|
||||
aspect-ratio: 1;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Right-to-Left Language Support */
|
||||
|
||||
.is-rtl .map-control {
|
||||
|
|
@ -262,3 +284,4 @@
|
|||
float: left;
|
||||
margin: 0 0 10px 10px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -233,13 +233,21 @@ div.v-dialog.v-dialog--fullscreen > div.v-card {
|
|||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
/* Lists and Menus */
|
||||
|
||||
.v-menu > .v-overlay__content > .v-list:not(.v-list--nav) {
|
||||
padding: 0;
|
||||
opacity: 0.97;
|
||||
}
|
||||
|
||||
.v-menu > .v-overlay__content > .v-list > .v-list-item.v-list-item--density-compact.v-list-item--one-line {
|
||||
line-height: 36px;
|
||||
min-height: 36px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
padding-inline: 10px;
|
||||
}
|
||||
|
||||
/* Progress */
|
||||
|
||||
.v-progress-linear,
|
||||
|
|
@ -417,12 +425,8 @@ div.v-dialog.v-dialog--fullscreen > div.v-card {
|
|||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.v-menu > .v-overlay__content > .v-list > .v-list-item.v-list-item--density-compact.v-list-item--one-line {
|
||||
line-height: 36px;
|
||||
min-height: 36px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
padding-inline: 10px;
|
||||
.v-input--readonly .v-autocomplete__menu-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.p-clipboard > .v-speed-dial__content > .v-btn.v-btn--icon {
|
||||
|
|
@ -557,6 +561,16 @@ form > .v-card > .v-card-text.dense {
|
|||
margin-inline-start: 12px;
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
|
||||
hr.v-divider {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.v-table hr {
|
||||
opacity: 0.05;
|
||||
}
|
||||
|
||||
.v-divider.v-divider--vertical {
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue