Compare commits

..

1 commit

Author SHA1 Message Date
Michael Mayer
baeba38acb UX: Proof-of-concept for new video player #1307 #3372
Signed-off-by: Michael Mayer <michael@photoprism.app>
2023-06-19 14:53:15 +02:00
636 changed files with 25337 additions and 45954 deletions

View file

@ -2,7 +2,7 @@
**By using the software and services we provide, you agree to our [Terms of Service](https://www.photoprism.app/terms), including our [Privacy Policy](https://www.photoprism.app/privacy) and the following Code of Conduct. It explains the "dos and donts" when interacting with our team and other community members.**
*Last Updated: July 5, 2023*
*Last Updated: May 13, 2023*
## Rules
@ -18,7 +18,7 @@ Because we want our Code of Conduct to be easy to understand and implement, we h
Not everyone has experience with Open Source communities and intuitively knows what is acceptable. In that case, the following guidelines and examples are meant to provide a quick overview and help you avoid the most common pitfalls:
(a) Do not [feel entitled](https://www.reddit.com/r/photoprism/comments/13emwf0/did_you_guys_really_nerf_hardware_transcoding/) to free software, support, or advice, especially if you are not a contributor, [member](https://link.photoprism.app/membership), or business customer. Don't expect contributors to [give status reports](https://docs.photoprism.app/developer-guide/code-quality/#go-slow-before-you-go-fast) as if they work for you or owe you something, even if you have donated a small amount. We also ask that you do not use GitHub Issues or other development tools to start general discussions, get technical support, or express personal opinions.
(a) Do not [feel entitled](https://www.reddit.com/r/photoprism/comments/13emwf0/did_you_guys_really_nerf_hardware_transcoding/) to free software, support, or advice, especially if you are not a contributor, [member](https://link.photoprism.app/membership), or paying customer. Don't expect contributors to report to you and [meet deadlines](https://docs.photoprism.app/developer-guide/code-quality/#go-slow-before-you-go-fast) as if they work for you or owe you something, even if you donated a small amount. We also ask that you do not use GitHub Issues or other development tools to start general discussions, get technical support, or express personal opinions.
(b) Honor **Rule &#35;2**, [read the docs](https://docs.photoprism.app) and [determine the cause of your problem](https://docs.photoprism.app/getting-started/troubleshooting/) before opening invalid bug reports, starting a public "shitstorm", or insulting other community members in our chat rooms. Aside from being annoying for everyone, it also keeps our team from working on features and enhancements that users like you are waiting for.

View file

@ -14,7 +14,7 @@ Feel free to [contact us](https://www.photoprism.app/contact) with anything that
You can also contribute by…
* answering questions in the [Community Chat](https://link.photoprism.app/chat), on [Reddit](https://link.photoprism.app/reddit) and in [GitHub Discussions](https://link.photoprism.app/discussions)
* helping us [translate](https://docs.photoprism.app/developer-guide/translations-weblate/) the Web UI on [Weblate](https://translate.photoprism.app/)
* helping us [translate](https://docs.photoprism.app/developer-guide/translations-weblate/) the Web UI
* [conducting research](https://github.com/photoprism/photoprism/issues?q=is%3Aopen+is%3Aissue+label%3Aresearch) and [improving the documentation](https://github.com/photoprism/photoprism/issues?q=is%3Aopen+is%3Aissue+label%3Adocs)
* publishing tutorials, blog posts, and podcasts
* voting for us on pages like:

View file

@ -1,9 +1,7 @@
# Ubuntu 23.10 (Mantic Minotaur)
FROM photoprism/develop:231206-mantic
# Ubuntu 23.04 (Lunar Lobster)
FROM photoprism/develop:230607-lunar
## Alternative Environments:
# FROM photoprism/develop:armv7 # ARMv7 (32bit)
# FROM photoprism/develop:lunar # Ubuntu 23.04 (Lunar Lobster)
# FROM photoprism/develop:jammy # Ubuntu 22.04 LTS (Jammy Jellyfish)
# FROM photoprism/develop:impish # Ubuntu 21.10 (Impish Indri)
# FROM photoprism/develop:bookworm # Debian 12 (Bookworm)
@ -15,4 +13,4 @@ WORKDIR "/go/src/github.com/photoprism/photoprism"
# Copy source to image.
COPY . .
COPY --chown=root:root /scripts/dist/ /scripts/
COPY --chown=root:root /scripts/dist/ /scripts/

113
Makefile
View file

@ -22,7 +22,7 @@ BUILD_ARCH ?= $(shell scripts/dist/arch.sh)
JS_BUILD_PATH ?= $(shell realpath "./assets/static/build")
# Install parameters.
INSTALL_PATH ?= $(BUILD_PATH)/photoprism-ce_$(BUILD_TAG)-$(shell echo $(BUILD_OS) | tr '[:upper:]' '[:lower:]')-$(BUILD_ARCH)
INSTALL_PATH ?= $(BUILD_PATH)/photoprism-$(BUILD_TAG)-$(shell echo $(BUILD_OS) | tr '[:upper:]' '[:lower:]')-$(BUILD_ARCH)
DESTDIR ?= $(INSTALL_PATH)
DESTUID ?= 1000
DESTGID ?= 1000
@ -54,7 +54,6 @@ all: dep build-js
dep: dep-tensorflow dep-js
biuld: build
build: build-go
build-all: build-go build-js
pull: docker-pull
test: test-js test-go
test-go: reset-sqlite run-test-go
@ -108,23 +107,19 @@ clean:
[ ! -d "$(JS_BUILD_PATH)" ] || rm -rf --preserve-root $(JS_BUILD_PATH)
tar.gz:
$(info Creating tar.gz archives from the directories in "$(BUILD_PATH)"...)
find "$(BUILD_PATH)" -maxdepth 1 -mindepth 1 -type d -name "photoprism*" -exec tar --exclude='.[^/]*' -C {} -czf {}.tar.gz . \;
pkg: pkg-amd64 pkg-arm64
pkg-amd64:
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" --entrypoint "" photoprism/develop:jammy make all install tar.gz
pkg-arm64:
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" --entrypoint "" photoprism/develop:jammy make all install tar.gz
find "$(BUILD_PATH)" -maxdepth 1 -mindepth 1 -type d -exec tar --exclude='.[^/]*' -C {} -czf {}.tar.gz . \;
install:
$(info Installing in "$(DESTDIR)"...)
@[ ! -d "$(DESTDIR)" ] || (echo "ERROR: Install path '$(DESTDIR)' already exists!"; exit 1)
mkdir --mode=$(INSTALL_MODE) -p $(DESTDIR)
env TMPDIR="$(BUILD_PATH)" ./scripts/dist/install-tensorflow.sh $(DESTDIR)
rm -rf --preserve-root $(DESTDIR)/include
(cd $(DESTDIR) && mkdir -p bin lib assets)
(cd $(DESTDIR) && mkdir -p bin lib assets config config/examples)
./scripts/build.sh prod "$(DESTDIR)/bin/$(BINARY_NAME)"
rsync -r -l --safe-links --exclude-from=assets/.buildignore --chmod=a+r,u+rw ./assets/ $(DESTDIR)/assets
wget -O $(DESTDIR)/assets/static/img/wallpaper/welcome.jpg https://cdn.photoprism.app/wallpaper/welcome.jpg
wget -O $(DESTDIR)/assets/static/img/preview.jpg https://cdn.photoprism.app/img/preview.jpg
cp internal/config/testdata/*.yml $(DESTDIR)/config/examples
chown -R $(INSTALL_USER) $(DESTDIR)
chmod -R $(INSTALL_MODE) $(DESTDIR)
chmod -R $(INSTALL_MODE_BIN) $(DESTDIR)/bin $(DESTDIR)/lib
@ -146,9 +141,9 @@ acceptance-sqlite-restart:
rm -rf storage/acceptance/originals/2011
rm -rf storage/acceptance/originals/2013
rm -rf storage/acceptance/originals/2017
./photoprism --auth-mode="public" -c "./storage/acceptance/config-sqlite" --test start -d
./photoprism -p -c "./storage/acceptance/config-sqlite" --test start -d
acceptance-sqlite-stop:
./photoprism --auth-mode="public" -c "./storage/acceptance/config-sqlite" --test stop
./photoprism -p -c "./storage/acceptance/config-sqlite" --test stop
acceptance-auth-sqlite-restart:
cp -f storage/acceptance/backup.db storage/acceptance/index.db
cp -f storage/acceptance/config-sqlite/settingsBackup.yml storage/acceptance/config-sqlite/settings.yml
@ -321,7 +316,7 @@ docker-develop: docker-develop-latest
docker-develop-all: docker-develop-latest docker-develop-other
docker-develop-latest: docker-develop-ubuntu
docker-develop-debian: docker-develop-bookworm docker-develop-bookworm-slim
docker-develop-ubuntu: docker-develop-mantic docker-develop-mantic-slim
docker-develop-ubuntu: docker-develop-lunar docker-develop-lunar-slim
docker-develop-other: docker-develop-debian docker-develop-bullseye docker-develop-bullseye-slim docker-develop-buster
docker-develop-bookworm:
docker pull --platform=amd64 debian:bookworm-slim
@ -339,9 +334,8 @@ docker-develop-bullseye-slim:
docker pull --platform=amd64 debian:bullseye-slim
docker pull --platform=arm64 debian:bullseye-slim
scripts/docker/buildx-multi.sh develop linux/amd64,linux/arm64 bullseye-slim /bullseye-slim
develop-armv7: docker-develop-armv7
docker-develop-armv7:
docker pull --platform=arm ubuntu:mantic
docker pull --platform=arm ubuntu:jammy
scripts/docker/buildx.sh develop linux/arm armv7 /armv7
docker-develop-buster:
docker pull --platform=amd64 golang:1-buster
@ -362,21 +356,13 @@ docker-develop-jammy-slim:
docker-develop-lunar:
docker pull --platform=amd64 ubuntu:lunar
docker pull --platform=arm64 ubuntu:lunar
scripts/docker/buildx-multi.sh develop linux/amd64,linux/arm64 lunar /lunar
scripts/docker/buildx-multi.sh develop linux/amd64,linux/arm64 lunar /lunar "-t photoprism/develop:latest -t photoprism/develop:ubuntu"
docker-develop-lunar-slim:
docker pull --platform=amd64 ubuntu:lunar
docker pull --platform=arm64 ubuntu:lunar
scripts/docker/buildx-multi.sh develop linux/amd64,linux/arm64 lunar-slim /lunar-slim
docker-develop-mantic:
docker pull --platform=amd64 ubuntu:mantic
docker pull --platform=arm64 ubuntu:mantic
scripts/docker/buildx-multi.sh develop linux/amd64,linux/arm64 mantic /mantic "-t photoprism/develop:latest -t photoprism/develop:ubuntu"
docker-develop-mantic-slim:
docker pull --platform=amd64 ubuntu:mantic
docker pull --platform=arm64 ubuntu:mantic
scripts/docker/buildx-multi.sh develop linux/amd64,linux/arm64 mantic-slim /mantic-slim
unstable: docker-unstable
docker-unstable: docker-unstable-mantic
docker-unstable: docker-unstable-lunar
docker-unstable-jammy:
docker pull --platform=amd64 photoprism/develop:jammy
docker pull --platform=amd64 photoprism/develop:jammy-slim
@ -385,17 +371,13 @@ docker-unstable-lunar:
docker pull --platform=amd64 photoprism/develop:lunar
docker pull --platform=amd64 photoprism/develop:lunar-slim
scripts/docker/buildx-multi.sh photoprism linux/amd64 unstable-ce /lunar
docker-unstable-mantic:
docker pull --platform=amd64 photoprism/develop:mantic
docker pull --platform=amd64 photoprism/develop:mantic-slim
scripts/docker/buildx-multi.sh photoprism linux/amd64 unstable-ce /mantic
preview: docker-preview-ce
docker-preview: docker-preview-ce
docker-preview-all: docker-preview-latest docker-preview-other
docker-preview-ce: docker-preview-mantic
docker-preview-ce: docker-preview-lunar
docker-preview-latest: docker-preview-ubuntu
docker-preview-debian: docker-preview-bookworm
docker-preview-ubuntu: docker-preview-mantic
docker-preview-ubuntu: docker-preview-lunar
docker-preview-other: docker-preview-debian docker-preview-bullseye
docker-preview-arm: docker-preview-arm64 docker-preview-armv7
docker-preview-bookworm:
@ -406,7 +388,7 @@ docker-preview-bookworm:
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 preview-bookworm /bookworm "-t photoprism/photoprism:preview-ce-debian"
docker-preview-armv7:
docker pull --platform=arm photoprism/develop:armv7
docker pull --platform=arm ubuntu:mantic
docker pull --platform=arm ubuntu:jammy
scripts/docker/buildx.sh photoprism linux/arm preview-armv7 /armv7
docker-preview-arm64:
docker pull --platform=arm64 photoprism/develop:lunar
@ -424,12 +406,6 @@ docker-preview-buster:
docker pull --platform=amd64 debian:buster-slim
docker pull --platform=arm64 debian:buster-slim
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 preview-buster /buster
docker-preview-impish:
docker pull --platform=amd64 photoprism/develop:impish
docker pull --platform=arm64 photoprism/develop:impish
docker pull --platform=amd64 ubuntu:impish
docker pull --platform=arm64 ubuntu:impish
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 preview-impish /impish
docker-preview-jammy:
docker pull --platform=amd64 photoprism/develop:jammy
docker pull --platform=amd64 photoprism/develop:jammy-slim
@ -442,18 +418,18 @@ docker-preview-lunar:
docker pull --platform=arm64 photoprism/develop:lunar
docker pull --platform=arm64 photoprism/develop:lunar-slim
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 preview-ce /lunar
docker-preview-mantic:
docker pull --platform=amd64 photoprism/develop:mantic
docker pull --platform=amd64 photoprism/develop:mantic-slim
docker pull --platform=arm64 photoprism/develop:mantic
docker pull --platform=arm64 photoprism/develop:mantic-slim
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 preview-ce /mantic
docker-preview-impish:
docker pull --platform=amd64 photoprism/develop:impish
docker pull --platform=arm64 photoprism/develop:impish
docker pull --platform=amd64 ubuntu:impish
docker pull --platform=arm64 ubuntu:impish
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 preview-impish /impish
release: docker-release
docker-release: docker-release-latest
docker-release-all: docker-release-latest docker-release-other
docker-release-latest: docker-release-ubuntu
docker-release-debian: docker-release-bookworm
docker-release-ubuntu: docker-release-mantic
docker-release-ubuntu: docker-release-lunar
docker-release-other: docker-release-debian docker-release-bullseye
docker-release-arm: docker-release-arm64 docker-release-armv7
docker-release-bookworm:
@ -464,7 +440,7 @@ docker-release-bookworm:
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 ce-bookworm /bookworm "-t photoprism/photoprism:ce-debian"
docker-release-armv7:
docker pull --platform=arm photoprism/develop:armv7
docker pull --platform=arm ubuntu:mantic
docker pull --platform=arm ubuntu:jammy
scripts/docker/buildx.sh photoprism linux/arm armv7 /armv7
docker-release-arm64:
docker pull --platform=arm64 photoprism/develop:lunar
@ -482,12 +458,6 @@ docker-release-buster:
docker pull --platform=amd64 debian:buster-slim
docker pull --platform=arm64 debian:buster-slim
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 ce-buster /buster
docker-release-impish:
docker pull --platform=amd64 photoprism/develop:impish
docker pull --platform=arm64 photoprism/develop:impish
docker pull --platform=amd64 ubuntu:impish
docker pull --platform=arm64 ubuntu:impish
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 ce-impish /impish
docker-release-jammy:
docker pull --platform=amd64 photoprism/develop:jammy
docker pull --platform=amd64 photoprism/develop:jammy-slim
@ -500,12 +470,12 @@ docker-release-lunar:
docker pull --platform=arm64 photoprism/develop:lunar
docker pull --platform=arm64 photoprism/develop:lunar-slim
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 ce /lunar
docker-release-mantic:
docker pull --platform=amd64 photoprism/develop:mantic
docker pull --platform=amd64 photoprism/develop:mantic-slim
docker pull --platform=arm64 photoprism/develop:mantic
docker pull --platform=arm64 photoprism/develop:mantic-slim
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 ce /mantic
docker-release-impish:
docker pull --platform=amd64 photoprism/develop:impish
docker pull --platform=arm64 photoprism/develop:impish
docker pull --platform=amd64 ubuntu:impish
docker pull --platform=arm64 ubuntu:impish
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 ce-impish /impish
start-local:
$(DOCKER_COMPOSE) -f docker-compose.local.yml up -d --wait
stop-local:
@ -532,8 +502,8 @@ terminal-latest:
$(DOCKER_COMPOSE) -f docker-compose.latest.yml exec photoprism-latest bash
logs-latest:
$(DOCKER_COMPOSE) -f docker-compose.latest.yml logs -f photoprism-latest
docker-local: docker-local-mantic
docker-local-all: docker-local-mantic docker-local-lunar docker-local-jammy docker-local-bookworm docker-local-bullseye docker-local-buster
docker-local: docker-local-lunar
docker-local-all: docker-local-lunar docker-local-jammy docker-local-bookworm docker-local-bullseye docker-local-buster
docker-local-bookworm:
docker pull photoprism/develop:bookworm
docker pull photoprism/develop:bookworm-slim
@ -546,10 +516,6 @@ docker-local-buster:
docker pull photoprism/develop:buster
docker pull debian:buster-slim
scripts/docker/build.sh photoprism ce-buster /buster "-t photoprism/photoprism:local"
docker-local-impish:
docker pull photoprism/develop:impish
docker pull ubuntu:impish
scripts/docker/build.sh photoprism ce-impish /impish "-t photoprism/photoprism:local"
docker-local-jammy:
docker pull photoprism/develop:jammy
docker pull ubuntu:jammy
@ -558,12 +524,12 @@ docker-local-lunar:
docker pull photoprism/develop:lunar
docker pull ubuntu:lunar
scripts/docker/build.sh photoprism ce-lunar /lunar "-t photoprism/photoprism:local"
docker-local-mantic:
docker pull photoprism/develop:mantic
docker pull ubuntu:mantic
scripts/docker/build.sh photoprism ce-mantic /mantic "-t photoprism/photoprism:local"
docker-local-develop: docker-local-develop-mantic
docker-local-develop-all: docker-local-develop-mantic docker-local-develop-lunar docker-local-develop-jammy docker-local-develop-bookworm docker-local-develop-bullseye docker-local-develop-buster docker-local-develop-impish
docker-local-impish:
docker pull photoprism/develop:impish
docker pull ubuntu:impish
scripts/docker/build.sh photoprism ce-impish /impish "-t photoprism/photoprism:local"
docker-local-develop: docker-local-develop-lunar
docker-local-develop-all: docker-local-develop-lunar docker-local-develop-jammy docker-local-develop-bookworm docker-local-develop-bullseye docker-local-develop-buster docker-local-develop-impish
docker-local-develop-bookworm:
docker pull debian:bookworm-slim
scripts/docker/build.sh develop bookworm /bookworm
@ -573,18 +539,15 @@ docker-local-develop-bullseye:
docker-local-develop-buster:
docker pull golang:1-buster
scripts/docker/build.sh develop buster /buster
docker-local-develop-impish:
docker pull ubuntu:impish
scripts/docker/build.sh develop impish /impish
docker-local-develop-jammy:
docker pull ubuntu:jammy
scripts/docker/build.sh develop jammy /jammy
docker-local-develop-lunar:
docker pull ubuntu:lunar
scripts/docker/build.sh develop lunar /lunar
docker-local-develop-mantic:
docker pull ubuntu:mantic
scripts/docker/build.sh develop mantic /mantic
docker-local-develop-impish:
docker pull ubuntu:impish
scripts/docker/build.sh develop impish /impish
docker-ddns:
docker pull golang:alpine
scripts/docker/buildx-multi.sh ddns linux/amd64,linux/arm64 $(BUILD_DATE)

View file

@ -2,19 +2,20 @@ PhotoPrism: Browse Your Life in Pictures
========================================
[![License: AGPL](https://img.shields.io/badge/license-AGPL-blue.svg)](https://docs.photoprism.app/license/agpl/)
[![GitHub contributors](https://img.shields.io/github/contributors/photoprism/photoprism.svg)](https://www.photoprism.app/about/team)
[![Documentation](https://img.shields.io/badge/read-the%20docs-4aa087.svg)](https://docs.photoprism.app/)
[![Community Chat](https://img.shields.io/badge/chat-on%20gitter-4aa087.svg)](https://link.photoprism.app/chat)
[![GitHub Discussions](https://img.shields.io/badge/ask-%20on%20github-4d6a91.svg)](https://link.photoprism.app/discussions)
[![Bluesky Social](https://dl.photoprism.app/img/badges/badge-follow-photoprism-bsky-social.svg)](https://photoprism.bsky.social/)
[![Mastodon](https://dl.photoprism.app/img/badges/badge-floss-social.svg)](https://floss.social/@photoprism)
[![Twitter](https://img.shields.io/badge/follow-@photoprism_app-00acee.svg)](https://link.photoprism.app/twitter)
[![Reddit](https://img.shields.io/badge/join-/r/photoprism-EC5800.svg)](https://link.photoprism.app/reddit)
PhotoPrism® is an AI-Powered Photos App for the [Decentralized Web](https://en.wikipedia.org/wiki/Decentralized_web).
It makes use of the latest technologies to tag and find pictures automatically without getting in your way.
You can run it at home, on a private server, or in the cloud.
![](https://dl.photoprism.app/img/ui/search-cards-view.jpg)
![](https://dl.photoprism.app/img/ui/desktop-1000px.jpg)
To get a first impression, you are welcome to play with our [public demo](https://try.photoprism.app/). Please be careful not to upload any private, unlawful or offensive pictures.
To get a first impression, you are welcome to play with our [public demo](https://try.photoprism.app/). Be careful not to upload any private pictures.
## Feature Overview ##
@ -38,12 +39,20 @@ Being completely [**self-funded and independent**](https://link.photoprism.app/m
## Getting Started ##
<img align="right" width="25%" src="https://www.photoprism.app/user/pages/01.home/03._screenshots/iphone-maps-hybrid-540px.png">
Step-by-step [installation instructions](https://docs.photoprism.app/getting-started/) for our self-hosted [community edition](https://link.photoprism.app/personal-editions) can be found on [docs.photoprism.app](https://docs.photoprism.app/getting-started/) - all you need is a Web browser and [Docker](https://docs.docker.com/get-docker/) to run the server. It is available for Mac, Linux, and Windows.
Step-by-step installation instructions for our self-hosted [community edition](https://www.photoprism.app/get) can be found
on [docs.photoprism.app](https://docs.photoprism.app/getting-started/) -
all you need is a Web browser and [Docker](https://docs.docker.com/get-docker/) to run the server.
It is available for Mac, Linux, and Windows.
The [stable releases](https://docs.photoprism.app/release-notes/) and [development preview](https://docs.photoprism.app/getting-started/updates/#development-preview) are available as a [multi-arch image](https://link.photoprism.app/docker-hub) for 64-bit AMD, Intel, and ARM processors.
That means, [Raspberry Pi](https://docs.photoprism.app/getting-started/raspberry-pi/) and Apple Silicon users enjoy the exact same functionality and can follow the same [installation steps](https://docs.photoprism.app/getting-started/docker-compose/).
The [stable version](https://docs.photoprism.app/release-notes/) and development
preview have been built into a single [multi-arch image](https://link.photoprism.app/docker-hub) for 64-bit AMD, Intel,
and ARM processors. That means, [Raspberry Pi](https://docs.photoprism.app/getting-started/raspberry-pi/) 3 / 4 owners can pull
from the same repository, enjoy the exact same functionality, and can follow the regular
[installation instructions](https://docs.photoprism.app/getting-started/docker-compose/)
after going through a short list of [requirements](https://docs.photoprism.app/getting-started/raspberry-pi/).
See our [Getting Started FAQ](https://docs.photoprism.app/getting-started/faq/#how-can-i-install-photoprism-without-docker) for alternative installation methods, for example using the [*tar.gz* packages](https://dl.photoprism.app/pkg/linux/README.html) we provide.
Existing users are advised to update their `docker-compose.yml` config based on our examples
available at [dl.photoprism.app/docker](https://dl.photoprism.app/docker/).
## Support Our Mission 💎 ##
@ -74,8 +83,9 @@ Please also leave [a star](https://github.com/photoprism/photoprism/stargazers)
## Getting Support ##
Visit [docs.photoprism.app/user-guide](https://docs.photoprism.app/user-guide/) to learn how to [sync](https://docs.photoprism.app/user-guide/sync/webdav/), [organize](https://docs.photoprism.app/user-guide/library/), and [share](https://docs.photoprism.app/user-guide/share/) your pictures. If you need help installing our software at home, you are welcome to post your question in [GitHub Discussions](https://link.photoprism.app/discussions) or ask in our [Community Chat](https://link.photoprism.app/chat).
Common problems can be quickly diagnosed and solved using our [Troubleshooting Checklists](https://docs.photoprism.app/getting-started/troubleshooting/). Eligible [members](https://link.photoprism.app/membership) are also welcome to email us for technical support and advice.
Visit [docs.photoprism.app/user-guide](https://docs.photoprism.app/user-guide/) to learn how to [sync](https://docs.photoprism.app/user-guide/sync/webdav/), [organize](https://docs.photoprism.app/user-guide/library/), and [share](https://docs.photoprism.app/user-guide/share/) your pictures. If you need help installing our software at home, you can [join us on Reddit](https://link.photoprism.app/reddit), ask in our [Community Chat](https://link.photoprism.app/chat), or post your question in [GitHub Discussions](https://link.photoprism.app/discussions).
Common problems can be quickly diagnosed and solved using the [Troubleshooting Checklists](https://docs.photoprism.app/getting-started/troubleshooting/) in [Getting Started](https://docs.photoprism.app/getting-started/). Eligible [members](https://link.photoprism.app/membership) are also welcome to [email us for technical support](https://www.photoprism.app/contact) and personalized advice.
## Upcoming Features and Enhancements ##
@ -103,7 +113,7 @@ Feel free to contact us at [hello@photoprism.app](mailto:hello@photoprism.app) w
## Every Contribution Makes a Difference ##
We welcome [contributions](CONTRIBUTING.md) of any kind, including blog posts, tutorials, translations, testing, writing documentation, and pull requests. Our [Developer Guide](https://docs.photoprism.app/developer-guide/) contains all the information necessary for you to get started.
We welcome [contributions](CONTRIBUTING.md) of any kind, including blog posts, tutorials, testing, writing documentation, and pull requests. Our [Developer Guide](https://docs.photoprism.app/developer-guide/) contains all the information necessary for you to get started.
----

View file

@ -20,10 +20,6 @@ You are [welcome to contact us](https://www.photoprism.app/contact) for change r
**Vitold Romanovski** (May 2023)
**Aaron C. de Bruyn** (September 2023)
[**Patrick Kvaksrud**](https://github.com/Kvaksrud) (October 2023)
## Gold Sponsors ##
[**Simen Eriksen**](https://github.com/dennorske) (GitHub Sponsors, December 2019)
@ -72,10 +68,6 @@ You are [welcome to contact us](https://www.photoprism.app/contact) for change r
[**Yongho Lee**](https://github.com/lyh16) (Patreon, May 2023)
**Albert R** (Patreon, August 2023)
**Peter Galbavy** (Patreon, November 2023)
## Infrastructure Sponsors ##
Our project infrastructure is provided by the following companies:

View file

@ -1,70 +0,0 @@
[{
"SourceFile": "beach_sand.jpg",
"ExifToolVersion": 12.56,
"FileName": "beach_sand.jpg",
"Directory": ".",
"FileSize": 105321,
"FileModifyDate": "2023:06:20 04:43:41+00:00",
"FileAccessDate": "2023:08:06 16:38:50+00:00",
"FileInodeChangeDate": "2023:08:04 05:10:37+00:00",
"FilePermissions": 100644,
"FileType": "JPEG",
"FileTypeExtension": "JPG",
"MIMEType": "image/jpeg",
"JFIFVersion": "1 2",
"ExifByteOrder": "MM",
"Make": "Apple",
"Model": "iPhone SE",
"XResolution": 72,
"YResolution": 72,
"ResolutionUnit": 2,
"Software": "10.2.1",
"ModifyDate": "2017:02:15 14:13:40",
"YCbCrPositioning": 1,
"ExposureTime": 0.0004940711462,
"ExposureProgram": 2,
"ISO": 25,
"ExifVersion": "0231",
"DateTimeOriginal": "2017:02:15 14:13:40",
"CreateDate": "2017:02:15 14:13:40",
"ComponentsConfiguration": "1 2 3 0",
"ApertureValue": 2.19999999733148,
"MeteringMode": 5,
"Flash": 16,
"FocalLength": 4.2,
"SubSecTimeOriginal": 249,
"SubSecTimeDigitized": 249,
"FlashpixVersion": "0100",
"ColorSpace": 65535,
"SensingMethod": 2,
"ExposureMode": 0,
"WhiteBalance": 0,
"SceneCaptureType": 0,
"LensModel": "iPhone SE back camera 4.15mm f/2.2",
"GPSVersionID": "2 3 0 0",
"GPSLatitudeRef": "S",
"GPSLongitudeRef": "E",
"GPSAltitudeRef": 1,
"CurrentIPTCDigest": "804bedc723e0e6cd3a41d0a44b074d19",
"DocumentNotes": "https://flickr.com/e/Rl7qi7oH%2BSEGDuwWBbZYaBQMEB5oNfPvQ6m3aMrPQ64%3D",
"ApplicationRecordVersion": 4,
"ImageWidth": 640,
"ImageHeight": 480,
"EncodingProcess": 2,
"BitsPerSample": 8,
"ColorComponents": 3,
"YCbCrSubSampling": "2 2",
"Aperture": 2.19999999733148,
"ImageSize": "640 480",
"Megapixels": 0.3072,
"ShutterSpeed": 0.0004940711462,
"SubSecCreateDate": "2017:02:15 14:13:40.249",
"SubSecDateTimeOriginal": "2017:02:15 14:13:40.249",
"GPSAltitude": -1.990417522,
"GPSLatitude": -29.2824777777778,
"GPSLongitude": 31.4436361111111,
"FocalLength35efl": 4.2,
"GPSPosition": "-29.2824777777778 31.4436361111111",
"LightValue": 15.2580006188259,
"LensID": "iPhone SE back camera 4.15mm f/2.2"
}]

Binary file not shown.

View file

@ -1,153 +0,0 @@
[{
"SourceFile": "iphone_15_pro.heic",
"ExifToolVersion": 12.40,
"FileName": "iphone_15_pro.heic",
"Directory": ".",
"FileSize": 886825,
"FileModifyDate": "2023:10:31 11:48:48+01:00",
"FileAccessDate": "2023:10:31 11:51:22+01:00",
"FileInodeChangeDate": "2023:10:31 11:50:11+01:00",
"FilePermissions": 100664,
"FileType": "HEIC",
"FileTypeExtension": "HEIC",
"MIMEType": "image/heic",
"MajorBrand": "heic",
"MinorVersion": "0.0.0",
"CompatibleBrands": ["mif1","MiHE","MiPr","miaf","MiHB","heic"],
"HandlerType": "pict",
"PrimaryItemReference": 49,
"MetaImageSize": "0 1287 4032 3024",
"XMPToolkit": "XMP Core 6.0.0",
"HDRGainMapHeadroom": 3.847906,
"HDRGainMapVersion": 131072,
"ExifByteOrder": "MM",
"Make": "Apple",
"Model": "iPhone 15 Pro",
"Orientation": 6,
"XResolution": 72,
"YResolution": 72,
"ResolutionUnit": 2,
"Software": 17.1,
"ModifyDate": "2023:10:31 11:44:43",
"HostComputer": "iPhone 15 Pro",
"ExposureTime": 0.01666666667,
"FNumber": 2.2,
"ExposureProgram": 2,
"ISO": 400,
"ExifVersion": "0232",
"DateTimeOriginal": "2023:10:31 11:44:43",
"CreateDate": "2023:10:31 11:44:43",
"OffsetTime": "+01:00",
"OffsetTimeOriginal": "+01:00",
"OffsetTimeDigitized": "+01:00",
"ShutterSpeedValue": 0.0165679999782223,
"ApertureValue": 2.20000000038133,
"BrightnessValue": 2.15000162,
"ExposureCompensation": 0,
"MeteringMode": 5,
"Flash": 16,
"FocalLength": 2.22,
"SubjectArea": "2015 1511 2323 1330",
"RunTimeFlags": 1,
"RunTimeValue": 154455128730208,
"RunTimeScale": 1000000000,
"RunTimeEpoch": 0,
"AccelerationVector": "-0.004213878416 -1.002046108 -0.01470538881",
"Warning": "[minor] Bad format (16) for MakerNotes entry 10",
"SubSecTimeOriginal": 432,
"SubSecTimeDigitized": 432,
"ColorSpace": 65535,
"ExifImageWidth": 4032,
"ExifImageHeight": 3024,
"SensingMethod": 2,
"SceneType": 1,
"ExposureMode": 0,
"WhiteBalance": 0,
"FocalLengthIn35mmFormat": 14,
"LensInfo": "2.22 9 1.779999971 2.8",
"LensMake": "Apple",
"LensModel": "iPhone 15 Pro back triple camera 2.22mm f/2.2",
"CompositeImage": 2,
"GPSLatitudeRef": "N",
"GPSLongitudeRef": "E",
"GPSAltitudeRef": 0,
"GPSTimeStamp": "10:44:43",
"GPSSpeedRef": "K",
"GPSSpeed": 0,
"GPSImgDirectionRef": "T",
"GPSImgDirection": 101.3112946,
"GPSDestBearingRef": "T",
"GPSDestBearing": 101.3112946,
"GPSDateStamp": "2023:10:31",
"GPSHPositioningError": 14.275587,
"ProfileCMMType": "appl",
"ProfileVersion": 1024,
"ProfileClass": "mntr",
"ColorSpaceData": "RGB ",
"ProfileConnectionSpace": "XYZ ",
"ProfileDateTime": "2022:01:01 00:00:00",
"ProfileFileSignature": "acsp",
"PrimaryPlatform": "APPL",
"CMMFlags": 0,
"DeviceManufacturer": "APPL",
"DeviceModel": "",
"DeviceAttributes": "0 0",
"RenderingIntent": 0,
"ConnectionSpaceIlluminant": "0.9642 1 0.82491",
"ProfileCreator": "appl",
"ProfileID": "236 253 163 142 56 133 71 195 109 180 189 79 122 218 24 47",
"ProfileDescription": "Display P3",
"ProfileCopyright": "Copyright Apple Inc., 2022",
"MediaWhitePoint": "0.96419 1 0.82489",
"RedMatrixColumn": "0.51512 0.2412 -0.00105",
"GreenMatrixColumn": "0.29198 0.69225 0.04189",
"BlueMatrixColumn": "0.1571 0.06657 0.78407",
"RedTRC": "(Binary data 32 bytes, use -b option to extract)",
"ChromaticAdaptation": "1.04788 0.02292 -0.0502 0.02959 0.99048 -0.01706 -0.00923 0.01508 0.75168",
"BlueTRC": "(Binary data 32 bytes, use -b option to extract)",
"GreenTRC": "(Binary data 32 bytes, use -b option to extract)",
"HEVCConfigurationVersion": 1,
"GeneralProfileSpace": 0,
"GeneralTierFlag": 0,
"GeneralProfileIDC": 3,
"GenProfileCompatibilityFlags": 1879048192,
"ConstraintIndicatorFlags": "176 0 0 0 0 0",
"GeneralLevelIDC": 90,
"MinSpatialSegmentationIDC": 0,
"ParallelismType": 0,
"ChromaFormat": 1,
"BitDepthLuma": 8,
"BitDepthChroma": 8,
"AverageFrameRate": 0,
"ConstantFrameRate": 0,
"NumTemporalLayers": 1,
"TemporalIDNested": 0,
"ImageWidth": 4032,
"ImageHeight": 3024,
"ImageSpatialExtent": "4032 3024",
"Rotation": 270,
"ImagePixelDepth": 8,
"AuxiliaryImageType": "urn:com:apple:photo:2020:aux:hdrgainmap",
"MediaDataSize": 882139,
"MediaDataOffset": 4686,
"RunTimeSincePowerUp": 154455.128730208,
"Aperture": 2.2,
"ImageSize": "4032 3024",
"Megapixels": 12.192768,
"ScaleFactor35efl": 6.30630630630631,
"ShutterSpeed": 0.01666666667,
"SubSecCreateDate": "2023:10:31 11:44:43.432+01:00",
"SubSecDateTimeOriginal": "2023:10:31 11:44:43.432+01:00",
"SubSecModifyDate": "2023:10:31 11:44:43+01:00",
"GPSAltitude": 50.15664187,
"GPSDateTime": "2023:10:31 10:44:43Z",
"GPSLatitude": 52.4596055555556,
"GPSLongitude": 13.3218416666667,
"CircleOfConfusion": "0.00476447847114884",
"FOV": 104.250120754113,
"FocalLength35efl": 14,
"GPSPosition": "52.4596055555556 13.3218416666667",
"HyperfocalDistance": 0.470184057236731,
"LightValue": 6.18189764281985,
"LensID": "iPhone 15 Pro back triple camera 2.22mm f/2.2"
}]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 MiB

View file

@ -1,82 +0,0 @@
[{
"SourceFile": "/go/src/github.com/photoprism/photoprism/storage/import/samsung-motion-photo.jpg",
"ExifToolVersion": 12.56,
"FileName": "samsung-motion-photo.jpg",
"Directory": "/go/src/github.com/photoprism/photoprism/storage/import",
"FileSize": 7221645,
"FileModifyDate": "2023:07:27 02:03:13+00:00",
"FileAccessDate": "2023:08:13 17:11:04+00:00",
"FileInodeChangeDate": "2023:08:13 17:11:02+00:00",
"FilePermissions": 100644,
"FileType": "JPEG",
"FileTypeExtension": "JPG",
"MIMEType": "image/jpeg",
"ExifByteOrder": "II",
"Make": "samsung",
"Model": "SM-G973F",
"Orientation": 1,
"XResolution": 72,
"YResolution": 72,
"ResolutionUnit": 2,
"Software": "G973FXXU4CTC9",
"ModifyDate": "2020:04:23 18:33:41",
"YCbCrPositioning": 1,
"ExposureTime": 0.0007575757576,
"FNumber": 2.4,
"ExposureProgram": 2,
"ISO": 50,
"ExifVersion": "0220",
"DateTimeOriginal": "2020:04:23 18:33:41",
"CreateDate": "2020:04:23 18:33:41",
"ShutterSpeedValue": 0.999475026346474,
"ApertureValue": 2.39495740923786,
"BrightnessValue": 22.58,
"ExposureCompensation": 0,
"MaxApertureValue": 2.39495740923786,
"MeteringMode": 2,
"Flash": 0,
"FocalLength": 4.32,
"ColorSpace": 1,
"ExifImageWidth": 4032,
"ExifImageHeight": 3024,
"ExposureMode": 0,
"WhiteBalance": 0,
"DigitalZoomRatio": 1,
"FocalLengthIn35mmFormat": 26,
"SceneCaptureType": 0,
"ImageUniqueID": "L12XLLD01VM",
"GPSLatitudeRef": "N",
"GPSLongitudeRef": "W",
"Compression": 6,
"ThumbnailOffset": 888,
"ThumbnailLength": 50555,
"XMPToolkit": "Adobe XMP Core 5.1.0-jc003",
"MicroVideo": 1,
"MicroVideoVersion": 1,
"MicroVideoOffset": 4535831,
"MicroVideoPresentationTimestampUs": -1,
"ImageWidth": 4032,
"ImageHeight": 3024,
"EncodingProcess": 0,
"BitsPerSample": 8,
"ColorComponents": 3,
"YCbCrSubSampling": "2 2",
"TimeStamp": "2020:04:23 17:33:41.809+00:00",
"MCCData": 234,
"EmbeddedVideoType": "MotionPhoto_Data",
"EmbeddedVideoFile": "(Binary data 4535775 bytes, use -b option to extract)",
"Aperture": 2.4,
"ImageSize": "4032 3024",
"Megapixels": 12.192768,
"ScaleFactor35efl": 6.01851851851852,
"ShutterSpeed": 0.0007575757576,
"ThumbnailImage": "(Binary data 50555 bytes, use -b option to extract)",
"GPSLatitude": 51.5049828,
"GPSLongitude": -0.0787347997222222,
"CircleOfConfusion": "0.00499230176602706",
"FOV": 69.3903656740024,
"FocalLength35efl": 26,
"GPSPosition": "51.5049828 -0.0787347997222222",
"HyperfocalDistance": 1.55759815100044,
"LightValue": 13.8923910258672
}]

View file

@ -2,16 +2,16 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-09 13:14+0000\n"
"PO-Revision-Date: 2023-10-16 16:35+0000\n"
"Last-Translator: dtsolakis <dtsola@eranet.gr>\n"
"POT-Creation-Date: 2023-02-09 12:51+0000\n"
"PO-Revision-Date: 2023-02-09 13:13+0000\n"
"Last-Translator: Anonymous <noreply@weblate.org>\n"
"Language-Team: none\n"
"Language: el\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.0.2\n"
"X-Generator: Weblate 4.15.2\n"
#: messages.go:94
msgid "Something went wrong, try again"
@ -19,7 +19,7 @@ msgstr "Κάτι πήγε στραβά, δοκιμάστε ξανά"
#: messages.go:95
msgid "Unable to do that"
msgstr "Αυτό δεν είναι εφικτό"
msgstr "Δεν είναι δυνατόν να το κάνετε αυτό"
#: messages.go:96
msgid "Changes could not be saved"
@ -334,7 +334,7 @@ msgstr "Επεξεργασία μεταφόρτωσης..."
#: messages.go:172
msgid "Upload has been processed"
msgstr "Η φόρτωση έχει ολοκληρωθεί"
msgstr "%s έχει αποκατασταθεί"
#: messages.go:173
msgid "Selection approved"

View file

@ -1,8 +1,8 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-09 13:14+0000\n"
"Report-Msgid-Bugs-To: ci@photoprism.app\n"
"POT-Creation-Date: 2023-02-09 12:51+0000\n"
"PO-Revision-Date: 2023-06-07 08:37+0000\n"
"Last-Translator: leosamuele221 <leosamuele221@gmail.com>\n"
"Language-Team: Italian <https://translate.photoprism.app/projects/photoprism/"

View file

@ -1,8 +1,8 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-09 13:14+0000\n"
"Report-Msgid-Bugs-To: ci@photoprism.app\n"
"POT-Creation-Date: 2023-02-09 12:51+0000\n"
"PO-Revision-Date: 2023-06-07 08:37+0000\n"
"Last-Translator: Admin <hello@photoprism.app>\n"
"Language-Team: Slovak <https://translate.photoprism.app/projects/photoprism/"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -26,8 +26,8 @@
<div class="splash-logo">
{{template "logo.gohtml" .}}
</div>
<progress id="progress" class="html-progress" max="100" style="accent-color: #c8c2e8; color-scheme: dark;"></progress>
<progress id="progress" class="html-progress" max="100"></progress>
</div>
</div>
<div id="busy-overlay"><div class="splash-center"><progress id="busy-progress" class="html-progress" max="100" style="accent-color: #c8c2e8; color-scheme: dark;"></progress></div></div>
<div id="busy-overlay"><div class="splash-center"><progress id="busy-progress" class="html-progress" max="100"></progress></div></div>

View file

@ -1,128 +0,0 @@
version: '3.5'
## FOR ARMv7 TEST AND DEVELOPMENT ONLY, DO NOT USE IN PRODUCTION ##
## Setup: https://docs.photoprism.app/developer-guide/setup/ ##
services:
## App Server
photoprism:
build: .
image: photoprism/photoprism:develop
platform: "arm"
depends_on:
- mariadb
- dummy-webdav
security_opt:
- seccomp:unconfined
- apparmor:unconfined
ports:
- "2342:2342" # Default HTTP port (host:container)
- "2343:2343" # Acceptance Test HTTP port (host:container)
shm_size: "2gb"
environment:
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # The initial admin password (min 4 characters)
PHOTOPRISM_UID: ${UID:-1000}
PHOTOPRISM_GID: ${GID:-1000}
PHOTOPRISM_SITE_URL: "http://localhost:2342/"
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "Browse Your Life"
PHOTOPRISM_SITE_DESCRIPTION: "AI-powered app for browsing, organizing & sharing your photo collection."
PHOTOPRISM_SITE_AUTHOR: "@photoprism_app"
PHOTOPRISM_DEBUG: "true"
PHOTOPRISM_READONLY: "false"
PHOTOPRISM_PUBLIC: "true"
PHOTOPRISM_EXPERIMENTAL: "true"
PHOTOPRISM_SERVER_MODE: "debug"
PHOTOPRISM_HTTP_HOST: "0.0.0.0"
PHOTOPRISM_HTTP_PORT: 2342
PHOTOPRISM_HTTP_COMPRESSION: "gzip" # Improves transfer speed and bandwidth utilization (none or gzip)
PHOTOPRISM_DATABASE_DRIVER: "mysql"
PHOTOPRISM_DATABASE_SERVER: "mariadb:4001"
PHOTOPRISM_DATABASE_NAME: "photoprism"
PHOTOPRISM_DATABASE_USER: "root"
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
PHOTOPRISM_TEST_DRIVER: "sqlite"
PHOTOPRISM_TEST_DSN: ".test.db"
PHOTOPRISM_TEST_DSN_MYSQL8: "root:photoprism@tcp(mysql:4001)/photoprism?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true"
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
PHOTOPRISM_DISABLE_CHOWN: "false" # Disables storage permission updates on startup
PHOTOPRISM_DISABLE_BACKUPS: "false" # Don't backup photo and album metadata to YAML files
PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server
PHOTOPRISM_DISABLE_SETTINGS: "false" # Disables Settings in Web UI
PHOTOPRISM_DISABLE_PLACES: "false" # Disables reverse geocoding and maps
PHOTOPRISM_DISABLE_EXIFTOOL: "false" # Don't create ExifTool JSON files for improved metadata extraction
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # Don't use TensorFlow for image classification
PHOTOPRISM_DETECT_NSFW: "false" # Flag photos as private that MAY be offensive (requires TensorFlow)
PHOTOPRISM_UPLOAD_NSFW: "false" # Allows uploads that may be offensive
PHOTOPRISM_DARKTABLE_PRESETS: "false" # Enables Darktable presets and disables concurrent RAW conversion
PHOTOPRISM_THUMB_FILTER: "lanczos" # Resample filter, best to worst: blackman, lanczos, cubic, linear
PHOTOPRISM_THUMB_UNCACHED: "true" # Enables on-demand thumbnail rendering (high memory and cpu usage)
PHOTOPRISM_THUMB_SIZE: 2048 # Pre-rendered thumbnail size limit (default 2048, min 720, max 7680)
# PHOTOPRISM_THUMB_SIZE: 4096 # Retina 4K, DCI 4K (requires more storage); 7680 for 8K Ultra HD
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)
PHOTOPRISM_JPEG_QUALITY: 92 # Set to 95 for high-quality thumbnails (25-100)
TF_CPP_MIN_LOG_LEVEL: 0 # Show TensorFlow log messages for development
## Enable TensorFlow AVX2 support for modern Intel CPUs (requires starting the container as root):
# PHOTOPRISM_INIT: "tensorflow-amd64-avx2"
## Hardware video transcoding config (optional):
# PHOTOPRISM_FFMPEG_BUFFERS: "64" # FFmpeg capture buffers (default: 32)
# PHOTOPRISM_FFMPEG_BITRATE: "32" # FFmpeg encoding bitrate limit in Mbit/s (default: 50)
# PHOTOPRISM_FFMPEG_ENCODER: "h264_v4l2m2m" # Use Video4Linux for AVC transcoding (default: libx264)
# PHOTOPRISM_FFMPEG_ENCODER: "h264_qsv" # Use Intel Quick Sync Video for AVC transcoding (default: libx264)
# PHOTOPRISM_INIT: "intel-graphics tensorflow-amd64-avx2" # Enable TensorFlow AVX2 & Intel Graphics support
# PHOTOPRISM_INIT: "install-updates" # Installs general operating system updates
## Hardware devices for video transcoding and machine learning (optional):
# devices:
# - "/dev/video11:/dev/video11" # Video4Linux (h264_v4l2m2m)
# - "/dev/dri/renderD128:/dev/dri/renderD128" # Intel GPU
# - "/dev/dri/card0:/dev/dri/card0"
working_dir: "/go/src/github.com/photoprism/photoprism"
volumes:
- ".:/go/src/github.com/photoprism/photoprism"
- "go-mod:/go/pkg/mod"
## MariaDB Database Server
## Docs: https://mariadb.com/docs/reference/
## Release Notes: https://mariadb.com/kb/en/changes-improvements-in-mariadb-1011/
mariadb:
image: mariadb:10.11
security_opt: # see https://github.com/MariaDB/mariadb-docker/issues/434#issuecomment-1136151239
- seccomp:unconfined
- apparmor:unconfined
command: --port=4001 --innodb-strict-mode=1 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001"
ports:
- "4001:4001" # database port (host:container)
volumes:
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MARIADB_AUTO_UPGRADE: "1"
MARIADB_INITDB_SKIP_TZINFO: "1"
MARIADB_DATABASE: "photoprism"
MARIADB_USER: "photoprism"
MARIADB_PASSWORD: "photoprism"
MARIADB_ROOT_PASSWORD: "photoprism"
## Dummy WebDAV Server
dummy-webdav:
image: photoprism/dummy-webdav:231015
container_name: dummy-webdav
environment:
WEBDAV_USERNAME: admin
WEBDAV_PASSWORD: photoprism
## Create named volume for Go module cache
volumes:
go-mod:
driver: local
## Create shared "photoprism-develop" network for connecting with services in other docker-compose.yml files
networks:
default:
name: shared
driver: bridge

View file

@ -22,7 +22,7 @@ services:
PHOTOPRISM_ADMIN_USER: "admin" # admin login username
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial admin password (8-72 characters)
PHOTOPRISM_AUTH_MODE: "public" # authentication mode (public, password)
PHOTOPRISM_SITE_URL: "http://localhost:2342/"
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Photo Management"
PHOTOPRISM_SITE_AUTHOR: "@photoprism_app"
@ -150,7 +150,7 @@ services:
security_opt: # see https://github.com/MariaDB/mariadb-docker/issues/434#issuecomment-1136151239
- seccomp:unconfined
- apparmor:unconfined
command: --port=4001 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
command: mariadbd --port=4001 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001" # database port (internal)
volumes:
@ -165,11 +165,11 @@ services:
## Dummy OpenID Connect Provider
dummy-oidc:
image: photoprism/dummy-oidc:231015
image: photoprism/dummy-oidc:220405
## Dummy WebDAV Server
dummy-webdav:
image: photoprism/dummy-webdav:231015
image: photoprism/dummy-webdav:20211109
environment:
WEBDAV_USERNAME: admin
WEBDAV_PASSWORD: photoprism

View file

@ -4,15 +4,15 @@ version: '3.5'
## Setup: https://docs.photoprism.app/developer-guide/setup/ ##
services:
## MariaDB 11.2 Database Server
## MariaDB 11.0 Database Server
## Docs: https://mariadb.com/docs/reference/
## Release Notes: https://mariadb.com/kb/en/release-notes-mariadb-11-2-series/
mariadb-11-2:
image: mariadb:11.2
## Release Notes: https://mariadb.com/kb/en/mariadb-11-0-0-release-notes/
mariadb-11-0:
image: mariadb:11.0-rc
security_opt: # see https://github.com/MariaDB/mariadb-docker/issues/434#issuecomment-1136151239
- seccomp:unconfined
- apparmor:unconfined
command: --port=4001 --innodb-strict-mode=1 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
command: mariadbd --port=4001 --innodb-strict-mode=1 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001"
ports:
@ -27,14 +27,14 @@ services:
MARIADB_PASSWORD: "photoprism"
MARIADB_ROOT_PASSWORD: "photoprism"
## MariaDB 10.11 Database Server
## Docs: https://mariadb.com/kb/en/release-notes-mariadb-1011-series/
mariadb-10-11:
image: mariadb:10.11
## MariaDB 10.8 Database Server
## Docs: https://mariadb.com/kb/en/release-notes-mariadb-108-series/
mariadb-10-8:
image: mariadb:10.8
security_opt: # see https://github.com/MariaDB/mariadb-docker/issues/434#issuecomment-1136151239
- seccomp:unconfined
- apparmor:unconfined
command: --port=4001 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
command: mariadbd --port=4001 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001"
ports:
@ -49,12 +49,28 @@ services:
MARIADB_PASSWORD: "photoprism"
MARIADB_ROOT_PASSWORD: "photoprism"
## MariaDB 10.7 Database Server
mariadb-10-7:
image: mariadb:10.7
command: mysqld --port=4001 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001" # database port (internal)
volumes:
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MARIADB_AUTO_UPGRADE: "1"
MARIADB_INITDB_SKIP_TZINFO: "1"
MARIADB_DATABASE: "photoprism"
MARIADB_USER: "photoprism"
MARIADB_PASSWORD: "photoprism"
MARIADB_ROOT_PASSWORD: "photoprism"
## MariaDB 10.5.5 Database Server
## Affected by MDEV-25362: Incorrect name resolution for subqueries in ON expressions
## see https://jira.mariadb.org/browse/MDEV-25362
mariadb-10-5-5:
image: mariadb:10.5.5
command: --port=4001 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
command: mysqld --port=4001 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001" # database port (internal)
volumes:
@ -69,7 +85,7 @@ services:
## Docs: https://mariadb.com/docs/reference/cs10.3/
mariadb-10-3:
image: mariadb:10.3
command: --port=4001 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
command: mysqld --port=4001 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001" # database port (internal)
volumes:
@ -84,7 +100,7 @@ services:
## Docs: https://mariadb.com/docs/reference/cs10.2/
mariadb-10-2:
image: mariadb:10.2
command: --port=4001 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
command: mysqld --port=4001 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001" # database port (internal)
volumes:
@ -98,7 +114,7 @@ services:
## MariaDB 10.1 Database Server
mariadb-10-1:
image: mariadb:10.1
command: --port=4001 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
command: mysqld --port=4001 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001" # database port (internal)
volumes:

View file

@ -8,7 +8,7 @@ services:
## Docs: https://dev.mysql.com/doc/refman/8.0/en/
mysql:
image: mysql:8
command: --port=4001 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
command: mysqld --port=4001 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001" # database port (internal)
environment:

View file

@ -30,7 +30,7 @@ services:
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)
PHOTOPRISM_SITE_URL: "http://localhost:2342/"
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Photo Management"
PHOTOPRISM_SITE_AUTHOR: "@photoprism_app"
@ -83,7 +83,7 @@ services:
## Dummy WebDAV Server
dummy-webdav:
image: photoprism/dummy-webdav:231015
image: photoprism/dummy-webdav:20211109
environment:
WEBDAV_USERNAME: admin
WEBDAV_PASSWORD: photoprism

View file

@ -62,7 +62,7 @@ services:
PHOTOPRISM_OIDC_CLIENT: "photoprism-develop"
PHOTOPRISM_OIDC_SECRET: "9d8351a0-ca01-4556-9c37-85eb634869b9"
## Site Information
PHOTOPRISM_SITE_URL: "http://localhost:2342/" # server URL in the format "http(s)://domain.name(:port)/(path)"
PHOTOPRISM_SITE_URL: "http://photoprism.me:2342/" # server URL in the format "http(s)://domain.name(:port)/(path)"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: "Tags and finds pictures without getting in your way!"
PHOTOPRISM_SITE_AUTHOR: "@photoprism_app"
@ -103,13 +103,13 @@ services:
PHOTOPRISM_JPEG_SIZE: 7680 # size limit for converted image files in pixels (720-30000)
PHOTOPRISM_JPEG_QUALITY: 85 # a higher value increases the quality and file size of JPEG images and thumbnails (25-100)
TF_CPP_MIN_LOG_LEVEL: 0 # show TensorFlow log messages for development
## Video Transcoding (https://docs.photoprism.app/getting-started/advanced/transcoding/):
# PHOTOPRISM_FFMPEG_ENCODER: "software" # 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: "32" # video bitrate limit in Mbit/s (default: 50)
# LIBVA_DRIVER_NAME: "i965" # For Intel architectures Haswell and older which do not support QSV yet but use VAAPI instead
## Run/install on first startup (options: update https gpu tensorflow davfs clitools clean):
PHOTOPRISM_INIT: "https tensorflow"
## Hardware Video Transcoding (optional):
# PHOTOPRISM_FFMPEG_ENCODER: "nvidia" # FFmpeg encoder ("software", "intel", "nvidia", "apple", "raspberry", "vaapi") Intel: "intel" for Broadwell or later and "vaapi" for Haswell or earlier
# PHOTOPRISM_FFMPEG_ENCODER: "intel" # FFmpeg encoder ("software", "intel", "nvidia", "apple", "raspberry", "vaapi") Intel: "intel" for Broadwell or later and "vaapi" for Haswell or earlier`
# PHOTOPRISM_FFMPEG_BITRATE: "32" # FFmpeg encoding bitrate limit in Mbit/s (default: 50)
# LIBVA_DRIVER_NAME: "i965" # For Intel architectures Haswell and older which do not support QSV yet but use VAAPI instead
## Share hardware devices with FFmpeg and TensorFlow (optional):
# devices:
# - "/dev/dri:/dev/dri" # Intel QSV (Broadwell and later) or VAAPI (Haswell and earlier)
@ -133,7 +133,7 @@ services:
security_opt: # see https://github.com/MariaDB/mariadb-docker/issues/434#issuecomment-1136151239
- seccomp:unconfined
- apparmor:unconfined
command: --port=4001 --innodb-strict-mode=1 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
command: mariadbd --port=4001 --innodb-strict-mode=1 --innodb-buffer-pool-size=256M --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001"
ports:
@ -165,7 +165,7 @@ services:
## Login: user / photoprism
## Admin: admin / photoprism
keycloak:
image: quay.io/keycloak/keycloak:22.0
image: quay.io/keycloak/keycloak:20.0
command: "start-dev" # development mode, do not use this in production!
container_name: keycloak
links:
@ -211,7 +211,7 @@ services:
## Dummy OpenID Connect Provider
dummy-oidc:
image: photoprism/dummy-oidc:231015
image: photoprism/dummy-oidc:220405
container_name: dummy-oidc
labels:
- "traefik.enable=true"
@ -224,7 +224,7 @@ services:
## Dummy WebDAV Server
dummy-webdav:
image: photoprism/dummy-webdav:231015
image: photoprism/dummy-webdav:220405
container_name: dummy-webdav
environment:
WEBDAV_USERNAME: admin

View file

@ -1,21 +1,64 @@
# Dockerfiles for Development and Production
# Dockerfiles and Docker Compose Examples
[**Dockerfiles**](https://docs.docker.com/engine/reference/builder/) are text documents that contain all commands a user could call in a terminal to assemble an application image.
[**Dockerfiles**](https://docs.docker.com/engine/reference/builder/) are text documents that contain all commands a user
could call in a terminal to assemble an application image.
[**Docker Compose**](https://docs.docker.com/compose/) uses [human-friendly YAML files](https://docs.photoprism.app/developer-guide/technologies/yaml/) to configure all application services so you can easily start them with a single command.
[**Docker Compose**](https://docs.docker.com/compose/) uses [human-friendly YAML files](https://docs.photoprism.app/developer-guide/technologies/yaml/)
to configure all application services so you can easily start them with a single command.
See our [Getting Started FAQ](https://docs.photoprism.app/getting-started/faq/#how-can-i-install-photoprism-without-docker) for alternative installation methods, for example using the [*tar.gz* packages](https://github.com/photoprism/photoprism/blob/develop/setup/pkg/linux/README.md) we provide for download at [dl.photoprism.app/pkg/linux/](https://dl.photoprism.app/pkg/linux/README.html).
## Why are we using Docker? ##
## What are the benefits of using Docker? ##
Containers are nothing new; [Solaris Zones](https://en.wikipedia.org/wiki/Solaris_Containers) have been around for
about 15 years, first released publicly in 2004. The chroot system call was introduced during
[development of Version 7 Unix in 1979](https://en.wikipedia.org/wiki/Chroot). It is used ever since for hosting
applications exposed to the public Internet.
**(1) Docker uses standard features of the Linux kernel.** Containers are nothing new; [Solaris Zones](https://en.wikipedia.org/wiki/Solaris_Containers) were released about 20 years ago and the chroot system call was introduced during [development of Version 7 Unix in 1979](https://en.wikipedia.org/wiki/Chroot). It is used ever since for hosting applications exposed to the public Internet. Modern Linux containers are an incremental improvement of this, based on standard functionality that is part of the kernel.
Modern Linux containers are an incremental enhancement. A main advantage of Docker is that application images
can be easily made available to users via Internet. It provides a common standard across most operating
systems and devices, which saves our team a lot of time that we can then spend [more effectively](https://docs.photoprism.app/developer-guide/issues/#effectiveness-efficiency), for example,
providing support and developing one of the many features that users are waiting for.
**(2) Docker saves time through simplified deployment and testing.** A main advantage of Docker is that application images can be [easily made available](https://hub.docker.com/r/photoprism/photoprism) to users via Internet. It provides a common standard across most operating systems and devices, which saves our team a lot of time that we can then spend [more effectively](https://docs.photoprism.app/developer-guide/code-quality/#effectiveness-efficiency), for example, providing support and developing one of the many features that users are waiting for.
Human-readable and versioned Dockerfiles as part of our public source code also help avoid "works for me" moments and
other unwelcome surprises by enabling teams to have the exact same environment everywhere in
[development](https://github.com/photoprism/photoprism/blob/develop/docker/develop/bookworm/Dockerfile), staging,
and [production](https://github.com/photoprism/photoprism/blob/develop/docker/photoprism/bookworm/Dockerfile).
**(3) Dockerfiles are part of the source code repository.** [Human-readable](https://docs.docker.com/engine/reference/builder/) and [versioned Dockerfiles](https://github.com/photoprism/photoprism/tree/develop/docker) that are part of our public source code help avoid "works for me" moments and other unwelcome surprises by enabling us to have the exact [same environment](http://localhost:8000/developer-guide/setup/) everywhere in [development](https://github.com/photoprism/photoprism/tree/develop/docker/develop), [staging, and production](https://github.com/photoprism/photoprism/tree/develop/docker/photoprism).
Last but not least, virtually all file format parsers have vulnerabilities that just haven't been discovered yet.
This is a known risk that can affect you even if your computer is not directly connected to the Internet.
Running apps in a container with limited host access is an easy way to improve security without
compromising performance and usability.
**(4) Running applications in containers is more secure.** Last but not least, virtually all file format parsers have vulnerabilities that just haven't been discovered yet. This is a known risk that can affect you even if your computer is not directly connected to the Internet. Running apps in a container with limited host access is an easy way to improve security without compromising performance and usability.
## What about Virtual Machines? ##
## Why not use virtual machines instead? ##
A virtual machine running its own operating system provides more security, but typically has side effects
such as lower performance and more difficult handling. You can also run Docker in a VM to get the best of
both worlds. It's essentially what happens when you run dockerized applications on [virtual cloud servers](https://docs.photoprism.app/getting-started/cloud/digitalocean/)
and operating systems other than Linux.
A virtual machine with a dedicated operating system environment provides even more security, but usually has side effects such as lower performance and more difficult handling. Using a VM, however, doesn't prevent you from running containerized apps to get the best of both worlds. This is essentially what happens when you install Docker on [virtual cloud servers](https://docs.photoprism.app/getting-started/cloud/digitalocean/) and operating systems other than Linux.
## Alternatives ##
### Building From Source ###
You can build and install PhotoPrism from the publicly available [source code](https://docs.photoprism.app/developer-guide/setup/):
```bash
git clone https://github.com/photoprism/photoprism.git
cd photoprism
make all install DESTDIR=/opt/photoprism
```
Missing build dependencies must be installed manually as shown in our human-readable and versioned
[Dockerfile](https://github.com/photoprism/photoprism/blob/develop/docker/develop/Dockerfile). You often don't
need to use the exact same versions, so it's possible to replace packages with what is available in your environment.
Please note that we do not have the resources to provide private users with dependencies and
[TensorFlow libraries](https://dl.photoprism.app/tensorflow/) for their personal environments. We recommend giving
Docker a try if you use Linux as it saves developers a lot of time when building, testing, and deploying complex
applications like PhotoPrism. It also effectively helps avoid "works for me" moments and missing dependencies.
### Installation Packages ###
An [unofficial port](https://docs.photoprism.app/getting-started/freebsd/) is available for FreeBSD / FreeNAS users.
You are invited to contribute by [building and testing standalone packages](https://docs.photoprism.app/developer-guide/) for Linux distributions and other operating systems.
Updates are [released several times a month](https://docs.photoprism.app/release-notes/), so maintaining the long list of dependencies for additional environments would currently consume too many of [our resources](https://link.photoprism.app/membership).

View file

@ -1,5 +1,5 @@
#### Base Image: Ubuntu 23.10 (Mantic Minotaur)
FROM ubuntu:mantic
#### Base Image: Ubuntu 22.04 LTS (Jammy Jellyfish)
FROM ubuntu:jammy
# Copyright © 2018 - 2023 PhotoPrism UG. All rights reserved.
#
@ -9,7 +9,7 @@ FROM ubuntu:mantic
# Add Open Container Initiative (OCI) annotations.
# See: https://github.com/opencontainers/image-spec/blob/main/annotations.md
LABEL org.opencontainers.image.title="PhotoPrism® Build Image (ARMv7)"
LABEL org.opencontainers.image.description="Ubuntu 23.10 (Mantic Minotaur)"
LABEL org.opencontainers.image.description="Ubuntu 22.04 LTS (Jammy Jellyfish)"
LABEL org.opencontainers.image.url="https://hub.docker.com/repository/docker/photoprism/develop"
LABEL org.opencontainers.image.source="https://github.com/photoprism/photoprism"
LABEL org.opencontainers.image.documentation="https://docs.photoprism.app/developer-guide/setup/"
@ -48,19 +48,19 @@ RUN echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80retries && \
echo 'APT::Get::Assume-Yes "true";' > /etc/apt/apt.conf.d/80forceyes && \
echo 'APT::Get::Fix-Missing "true";' > /etc/apt/apt.conf.d/80fixmissing && \
echo 'force-confold' > /etc/dpkg/dpkg.cfg.d/force-confold && \
apt-get update && apt-get -qq dist-upgrade && apt-get -qq install --no-install-recommends \
apt-get update && apt-get -qq upgrade && apt-get -qq install --no-install-recommends \
apt-utils gpg pkg-config software-properties-common ca-certificates avahi-utils \
build-essential gcc g++ sudo bash make nano lsof lshw git jq \
autoconf automake cmake libtool libjpeg-dev libpng-dev libwebp-dev \
autoconf automake cmake libtool libjpeg8 libjpeg8-dev \
zip unzip wget curl rsync sqlite3 chrpath gettext libc6-dev \
libssl-dev libxft-dev libfreetype6 libfreetype6-dev libfontconfig1 \
libfontconfig1-dev libhdf5-serial-dev libzmq3-dev \
libfontconfig1-dev libhdf5-serial-dev libpng-dev libzmq3-dev \
libx264-dev libx265-dev libde265-dev libaom-dev libnss3 libxtst6 librsvg2-bin tzdata \
exiftool ffmpeg libavcodec-extra \
&& \
/scripts/install-nodejs.sh && \
/scripts/install-libheif.sh && \
/scripts/install-tensorflow.sh && \
/scripts/install-libheif.sh && \
/scripts/install-go.sh && \
/scripts/install-go-tools.sh && \
echo 'alias ll="ls -alh"' >> /etc/skel/.bashrc && \
@ -79,14 +79,14 @@ RUN echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80retries && \
/photoprism/storage/cache && \
/scripts/cleanup.sh
# Download models and testdata.
# download models and testdata
RUN mkdir /tmp/photoprism && \
wget "https://dl.photoprism.app/tensorflow/nsfw.zip?${BUILD_TAG}" -O /tmp/photoprism/nsfw.zip && \
wget "https://dl.photoprism.app/tensorflow/nasnet.zip?${BUILD_TAG}" -O /tmp/photoprism/nasnet.zip && \
wget "https://dl.photoprism.app/tensorflow/facenet.zip?${BUILD_TAG}" -O /tmp/photoprism/facenet.zip && \
wget "https://dl.photoprism.app/qa/testdata.zip?${BUILD_TAG}" -O /tmp/photoprism/testdata.zip
# Default working directory.
# set up project directory
WORKDIR "/go/src/github.com/photoprism/photoprism"
# Expose the following container ports:
@ -103,4 +103,3 @@ ENTRYPOINT ["/scripts/entrypoint.sh"]
# Keep container running.
CMD ["tail", "-f", "/dev/null"]

View file

@ -43,7 +43,7 @@ RUN echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80retries && \
echo 'APT::Get::Assume-Yes "true";' > /etc/apt/apt.conf.d/80forceyes && \
echo 'APT::Get::Fix-Missing "true";' > /etc/apt/apt.conf.d/80fixmissing && \
echo 'force-confold' > /etc/dpkg/dpkg.cfg.d/force-confold && \
apt-get update && apt-get -qq dist-upgrade && \
apt-get update && apt-get -qq upgrade && \
apt-get -qq install \
libc6 ca-certificates sudo bash tzdata \
gpg zip unzip wget curl rsync make nano \

View file

@ -49,7 +49,7 @@ RUN echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80retries && \
echo 'APT::Get::Assume-Yes "true";' > /etc/apt/apt.conf.d/80forceyes && \
echo 'APT::Get::Fix-Missing "true";' > /etc/apt/apt.conf.d/80fixmissing && \
echo 'force-confold' > /etc/dpkg/dpkg.cfg.d/force-confold && \
apt-get update && apt-get -qq dist-upgrade && \
apt-get update && apt-get -qq upgrade && \
apt-get -qq install \
libc6 ca-certificates sudo bash tzdata \
gpg zip unzip wget curl rsync make nano \

View file

@ -42,7 +42,7 @@ RUN echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80retries && \
echo 'APT::Get::Assume-Yes "true";' > /etc/apt/apt.conf.d/80forceyes && \
echo 'APT::Get::Fix-Missing "true";' > /etc/apt/apt.conf.d/80fixmissing && \
echo 'force-confold' > /etc/dpkg/dpkg.cfg.d/force-confold && \
apt-get update && apt-get -qq dist-upgrade && \
apt-get update && apt-get -qq upgrade && \
apt-get -qq install \
libc6 ca-certificates bash sudo nano avahi-utils jq lsof lshw \
exiftool sqlite3 tzdata gpg make zip unzip wget curl rsync \

View file

@ -48,7 +48,7 @@ RUN echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80retries && \
echo 'APT::Get::Assume-Yes "true";' > /etc/apt/apt.conf.d/80forceyes && \
echo 'APT::Get::Fix-Missing "true";' > /etc/apt/apt.conf.d/80fixmissing && \
echo 'force-confold' > /etc/dpkg/dpkg.cfg.d/force-confold && \
apt-get update && apt-get -qq dist-upgrade && \
apt-get update && apt-get -qq upgrade && \
apt-get -qq install \
libc6 ca-certificates bash sudo nano avahi-utils jq lsof lshw \
exiftool sqlite3 tzdata gpg make zip unzip wget curl rsync \
@ -58,11 +58,11 @@ RUN echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80retries && \
apt-get -qq install \
apt-utils pkg-config software-properties-common \
build-essential gcc g++ git gettext davfs2 chrpath apache2-utils \
autoconf automake cmake libtool libjpeg-dev libpng-dev libwebp-dev \
libx264-dev libx265-dev libaom-dev libvpx-dev libwebm-dev libxft-dev \
autoconf automake cmake libtool libjpeg8-dev \
libx264-dev libx265-dev libaom-dev libvpx-dev libwebm-dev libpng-dev libxft-dev \
libc6-dev libhdf5-serial-dev libzmq3-dev libssl-dev libnss3 \
libfreetype6 libfreetype6-dev libfontconfig1 libfontconfig1-dev fonts-roboto \
librsvg2-bin ghostscript gsfonts pdf2svg ps2eps \
librsvg2-bin ghostscript gsfonts \
&& \
/scripts/install-nodejs.sh && \
/scripts/install-mariadb.sh mariadb-client && \

View file

@ -42,12 +42,12 @@ RUN echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80retries && \
echo 'APT::Get::Assume-Yes "true";' > /etc/apt/apt.conf.d/80forceyes && \
echo 'APT::Get::Fix-Missing "true";' > /etc/apt/apt.conf.d/80fixmissing && \
echo 'force-confold' > /etc/dpkg/dpkg.cfg.d/force-confold && \
apt-get update && apt-get -qq dist-upgrade && \
apt-get update && apt-get -qq upgrade && \
apt-get -qq install \
libc6 ca-certificates bash sudo nano avahi-utils jq lsof lshw libebml5 libgav1-bin libatomic1 \
exiftool sqlite3 tzdata gpg make zip unzip wget curl rsync imagemagick libvips-dev rawtherapee \
ffmpeg libffmpeg-nvenc-dev libswscale-dev libavfilter-extra libavformat-extra libavcodec-extra \
x264 x265 libde265-dev libaom3 libvpx7 libwebm1 libjpeg8 libmatroska7 libdvdread8 \
libc6 ca-certificates bash sudo nano avahi-utils jq lsof lshw \
exiftool sqlite3 tzdata gpg make zip unzip wget curl rsync \
imagemagick libvips-dev rawtherapee ffmpeg libavcodec-extra x264 x265 libde265-dev \
libaom3 libvpx7 libwebm1 libjpeg8 libmatroska7 libdvdread8 libebml5 libgav1-bin libatomic1 \
&& \
/scripts/install-mariadb.sh mariadb-client && \
/scripts/install-darktable.sh && \
@ -75,4 +75,4 @@ WORKDIR /photoprism
EXPOSE 2342 2442 2443
# Keep container running.
CMD ["tail", "-f", "/dev/null"]
CMD ["tail", "-f", "/dev/null"]

View file

@ -48,21 +48,21 @@ RUN echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80retries && \
echo 'APT::Get::Assume-Yes "true";' > /etc/apt/apt.conf.d/80forceyes && \
echo 'APT::Get::Fix-Missing "true";' > /etc/apt/apt.conf.d/80fixmissing && \
echo 'force-confold' > /etc/dpkg/dpkg.cfg.d/force-confold && \
apt-get update && apt-get -qq dist-upgrade && \
apt-get update && apt-get -qq upgrade && \
apt-get -qq install \
libc6 ca-certificates bash sudo nano avahi-utils jq lsof lshw libebml5 libgav1-bin libatomic1 \
exiftool sqlite3 tzdata gpg make zip unzip wget curl rsync imagemagick libvips-dev rawtherapee \
ffmpeg libffmpeg-nvenc-dev libswscale-dev libavfilter-extra libavformat-extra libavcodec-extra \
x264 x265 libde265-dev libaom3 libvpx7 libwebm1 libjpeg8 libmatroska7 libdvdread8 \
libc6 ca-certificates bash sudo nano avahi-utils jq lsof lshw \
exiftool sqlite3 tzdata gpg make zip unzip wget curl rsync \
imagemagick libvips-dev rawtherapee ffmpeg libavcodec-extra x264 x265 libde265-dev \
libaom3 libvpx7 libwebm1 libjpeg8 libmatroska7 libdvdread8 libebml5 libgav1-bin libatomic1 \
&& \
apt-get -qq install \
apt-utils pkg-config software-properties-common \
build-essential gcc g++ git gettext davfs2 chrpath apache2-utils \
autoconf automake cmake libtool libjpeg-dev libpng-dev libwebp-dev \
libx264-dev libx265-dev libaom-dev libvpx-dev libwebm-dev libxft-dev \
autoconf automake cmake libtool libjpeg8-dev \
libx264-dev libx265-dev libaom-dev libvpx-dev libwebm-dev libpng-dev libxft-dev \
libc6-dev libhdf5-serial-dev libzmq3-dev libssl-dev libnss3 \
libfreetype6 libfreetype6-dev libfontconfig1 libfontconfig1-dev fonts-roboto \
librsvg2-bin ghostscript gsfonts pdf2svg ps2eps \
librsvg2-bin ghostscript gsfonts \
&& \
/scripts/install-nodejs.sh && \
/scripts/install-mariadb.sh mariadb-client && \

View file

@ -1,78 +0,0 @@
#### Base Image: Ubuntu 23.10 (Mantic Minotaur)
FROM ubuntu:mantic
# Copyright © 2018 - 2023 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/
# Add Open Container Initiative (OCI) annotations.
# See: https://github.com/opencontainers/image-spec/blob/main/annotations.md
LABEL org.opencontainers.image.title="PhotoPrism® Base Image (Ubuntu 23.10)"
LABEL org.opencontainers.image.description="Ubuntu 23.10 (Mantic Minotaur)"
LABEL org.opencontainers.image.url="https://hub.docker.com/repository/docker/photoprism/develop"
LABEL org.opencontainers.image.source="https://github.com/photoprism/photoprism"
LABEL org.opencontainers.image.documentation="https://docs.photoprism.app/developer-guide/setup/"
LABEL org.opencontainers.image.authors="PhotoPrism UG <hello@photoprism.app>"
LABEL org.opencontainers.image.vendor="PhotoPrism UG"
# Declare build parameters.
ARG TARGETARCH
ARG BUILD_TAG
# Set environment variables, see https://docs.photoprism.app/getting-started/config-options/.
ENV PHOTOPRISM_ARCH=$TARGETARCH \
DOCKER_TAG=$BUILD_TAG \
DOCKER_ENV="prod" \
PS1="\u@$DOCKER_TAG:\w\$ " \
PATH="/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin:/scripts:/opt/photoprism/bin" \
LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \
TMPDIR="/tmp" \
DEBIAN_FRONTEND="noninteractive" \
TF_CPP_MIN_LOG_LEVEL="2" \
PROG="photoprism"
# Copy scripts and package sources config.
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
# Update base image and add dependencies.
RUN echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80retries && \
echo 'APT::Install-Recommends "false";' > /etc/apt/apt.conf.d/80recommends && \
echo 'APT::Install-Suggests "false";' > /etc/apt/apt.conf.d/80suggests && \
echo 'APT::Get::Assume-Yes "true";' > /etc/apt/apt.conf.d/80forceyes && \
echo 'APT::Get::Fix-Missing "true";' > /etc/apt/apt.conf.d/80fixmissing && \
echo 'force-confold' > /etc/dpkg/dpkg.cfg.d/force-confold && \
apt-get update && apt-get -qq dist-upgrade && \
apt-get -qq install \
libc6 ca-certificates bash sudo nano avahi-utils jq lsof lshw libebml5 libgav1-bin libatomic1 \
exiftool sqlite3 tzdata gpg make zip unzip wget curl rsync imagemagick libvips-dev rawtherapee \
ffmpeg libffmpeg-nvenc-dev libswscale-dev libavfilter-extra libavformat-extra libavcodec-extra \
x264 x265 libde265-dev libaom3 libvpx7 libwebm1 libjpeg8 libmatroska7 libdvdread8 \
&& \
/scripts/install-mariadb.sh mariadb-client && \
/scripts/install-darktable.sh && \
/scripts/install-libheif.sh && \
echo 'alias ll="ls -alh"' >> /etc/skel/.bashrc && \
echo 'export PS1="\u@$DOCKER_TAG:\w\$ "' >> /etc/skel/.bashrc && \
echo "ALL ALL=(ALL) NOPASSWD:SETENV: /scripts/entrypoint-init.sh" >> /etc/sudoers.d/init && \
cp /etc/skel/.bashrc /root/.bashrc && \
/scripts/create-users.sh && \
install -d -m 0777 -o 1000 -g 1000 \
/photoprism/originals \
/photoprism/import \
/photoprism/storage \
/photoprism/storage/sidecar \
/photoprism/storage/albums \
/photoprism/storage/backups \
/photoprism/storage/config \
/photoprism/storage/cache && \
/scripts/cleanup.sh
# Default working directory.
WORKDIR /photoprism
# Expose HTTP and HTTPS ports.
EXPOSE 2342 2442 2443
# Keep container running.
CMD ["tail", "-f", "/dev/null"]

View file

@ -1,115 +0,0 @@
#### Base Image: Ubuntu 23.10 (Mantic Minotaur)
FROM ubuntu:mantic
# Copyright © 2018 - 2023 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/
# Add Open Container Initiative (OCI) annotations.
# See: https://github.com/opencontainers/image-spec/blob/main/annotations.md
LABEL org.opencontainers.image.title="PhotoPrism® Build Image (Ubuntu 23.10)"
LABEL org.opencontainers.image.description="Ubuntu 23.10 (Mantic Minotaur)"
LABEL org.opencontainers.image.url="https://hub.docker.com/repository/docker/photoprism/develop"
LABEL org.opencontainers.image.source="https://github.com/photoprism/photoprism"
LABEL org.opencontainers.image.documentation="https://docs.photoprism.app/developer-guide/setup/"
LABEL org.opencontainers.image.authors="PhotoPrism UG <hello@photoprism.app>"
LABEL org.opencontainers.image.vendor="PhotoPrism UG"
# Declare build parameters.
ARG TARGETARCH
ARG BUILD_TAG
# Set environment variables, see https://docs.photoprism.app/getting-started/config-options/.
ENV PHOTOPRISM_ARCH=$TARGETARCH \
DOCKER_TAG=$BUILD_TAG \
DOCKER_ENV="develop" \
NODE_ENV="production" \
PS1="\u@$DOCKER_TAG:\w\$ " \
PATH="/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin:/scripts:/usr/local/go/bin:/go/bin:/opt/photoprism/bin" \
LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \
DEBIAN_FRONTEND="noninteractive" \
TMPDIR="/tmp" \
TF_CPP_MIN_LOG_LEVEL="0" \
GOPATH="/go" \
GOBIN="/usr/local/bin" \
GO111MODULE="on" \
CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \
PROG="photoprism"
# Copy scripts and package sources config.
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
COPY --chown=root:root --chmod=644 /.my.cnf /etc/my.cnf
# Update base image and add dependencies.
RUN echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80retries && \
echo 'APT::Install-Recommends "false";' > /etc/apt/apt.conf.d/80recommends && \
echo 'APT::Install-Suggests "false";' > /etc/apt/apt.conf.d/80suggests && \
echo 'APT::Get::Assume-Yes "true";' > /etc/apt/apt.conf.d/80forceyes && \
echo 'APT::Get::Fix-Missing "true";' > /etc/apt/apt.conf.d/80fixmissing && \
echo 'force-confold' > /etc/dpkg/dpkg.cfg.d/force-confold && \
apt-get update && apt-get -qq dist-upgrade && \
apt-get -qq install \
libc6 ca-certificates bash sudo nano avahi-utils jq lsof lshw libebml5 libgav1-bin libatomic1 \
exiftool sqlite3 tzdata gpg make zip unzip wget curl rsync imagemagick libvips-dev rawtherapee \
ffmpeg libffmpeg-nvenc-dev libswscale-dev libavfilter-extra libavformat-extra libavcodec-extra \
x264 x265 libde265-dev libaom3 libvpx7 libwebm1 libjpeg8 libmatroska7 libdvdread8 \
&& \
apt-get -qq install \
apt-utils pkg-config software-properties-common \
build-essential gcc g++ git gettext davfs2 chrpath apache2-utils \
autoconf automake cmake libtool libjpeg-dev libpng-dev libwebp-dev \
libx264-dev libx265-dev libaom-dev libvpx-dev libwebm-dev libxft-dev \
libc6-dev libhdf5-serial-dev libzmq3-dev libssl-dev libnss3 \
libfreetype6 libfreetype6-dev libfontconfig1 libfontconfig1-dev fonts-roboto \
librsvg2-bin ghostscript gsfonts pdf2svg ps2eps \
&& \
/scripts/install-nodejs.sh && \
/scripts/install-mariadb.sh mariadb-client && \
/scripts/install-tensorflow.sh && \
/scripts/install-darktable.sh && \
/scripts/install-libheif.sh && \
/scripts/install-chrome.sh && \
/scripts/install-go.sh && \
/scripts/install-go-tools.sh && \
echo 'alias go=richgo ll="ls -alh"' >> /etc/skel/.bashrc && \
echo 'export PS1="\u@$DOCKER_TAG:\w\$ "' >> /etc/skel/.bashrc && \
echo "ALL ALL=(ALL) NOPASSWD:SETENV: ALL" >> /etc/sudoers.d/all && \
cp /etc/skel/.bashrc /root/.bashrc && \
cp /scripts/convert/policy.xml /etc/ImageMagick-6/policy.xml && \
/scripts/create-users.sh && \
install -d -m 0777 -o 1000 -g 1000 \
/photoprism/originals \
/photoprism/import \
/photoprism/storage \
/photoprism/storage/sidecar \
/photoprism/storage/albums \
/photoprism/storage/backups \
/photoprism/storage/config \
/photoprism/storage/cache && \
/scripts/cleanup.sh
# Download models and testdata.
RUN mkdir /tmp/photoprism && \
wget "https://dl.photoprism.app/tensorflow/nsfw.zip?${BUILD_TAG}" -O /tmp/photoprism/nsfw.zip && \
wget "https://dl.photoprism.app/tensorflow/nasnet.zip?${BUILD_TAG}" -O /tmp/photoprism/nasnet.zip && \
wget "https://dl.photoprism.app/tensorflow/facenet.zip?${BUILD_TAG}" -O /tmp/photoprism/facenet.zip && \
wget "https://dl.photoprism.app/qa/testdata.zip?${BUILD_TAG}" -O /tmp/photoprism/testdata.zip
# Default working directory.
WORKDIR "/go/src/github.com/photoprism/photoprism"
# Expose the following container ports:
# - 2342 (HTTP)
# - 2343 (Acceptance Tests)
# - 2442 (HTTP)
# - 2443 (HTTPS)
# - 9515 (Chromedriver)
# - 40000 (Go Debugger)
EXPOSE 2342 2343 2442 2443 9515 40000
# Declare container entrypoint script.
ENTRYPOINT ["/scripts/entrypoint.sh"]
# Keep container running.
CMD ["tail", "-f", "/dev/null"]

View file

@ -8,10 +8,9 @@ FROM photoprism/develop:armv7 as build
# Declare build parameters.
ARG TARGETARCH
ARG TARGETPLATFORM
ARG BUILD_TAG
# Copy project files.
# Copy source to image.
WORKDIR "/go/src/github.com/photoprism/photoprism"
COPY . .
@ -19,19 +18,18 @@ COPY . .
RUN make all install DESTDIR=/opt/photoprism
################################################## PRODUCTION STAGE ####################################################
#### Base Image: Ubuntu 23.10 (Mantic Minotaur)
FROM ubuntu:mantic
#### Base Image: Ubuntu 22.04 LTS (Jammy Jellyfish)
FROM ubuntu:jammy
# Add Open Container Initiative (OCI) annotations.
# See: https://github.com/opencontainers/image-spec/blob/main/annotations.md
LABEL org.opencontainers.image.title="PhotoPrism® (ARMv7)"
LABEL org.opencontainers.image.description="Ubuntu 23.10 (Mantic Minotaur)"
LABEL org.opencontainers.image.description="Ubuntu 22.04 LTS (Jammy Jellyfish)"
LABEL org.opencontainers.image.url="https://hub.docker.com/r/photoprism/photoprism"
LABEL org.opencontainers.image.source="https://github.com/photoprism/photoprism"
LABEL org.opencontainers.image.documentation="https://docs.photoprism.app/getting-started/"
LABEL org.opencontainers.image.authors="PhotoPrism UG <hello@photoprism.app>"
LABEL org.opencontainers.image.vendor="PhotoPrism UG"
LABEL org.opencontainers.image.ref.name="photoprism"
# Declare build parameters.
ARG TARGETARCH
@ -61,7 +59,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_UPLOAD_NSFW="true" \
PHOTOPRISM_DETECT_NSFW="false" \
PHOTOPRISM_EXPERIMENTAL="false" \
PHOTOPRISM_SITE_URL="http://localhost:2342/" \
PHOTOPRISM_SITE_URL="http://photoprism.me:2342/" \
PHOTOPRISM_SITE_CAPTION="AI-Powered Photos App" \
PHOTOPRISM_SITE_DESCRIPTION="" \
PHOTOPRISM_SITE_AUTHOR="" \
@ -91,8 +89,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_WORKERS=0 \
PHOTOPRISM_WAKEUP_INTERVAL=900 \
PHOTOPRISM_AUTO_INDEX=300 \
PHOTOPRISM_AUTO_IMPORT=300 \
PHOTOPRISM_INIT="https"
PHOTOPRISM_AUTO_IMPORT=300
# Copy dist files, scripts, and debian backports sources list.
COPY --from=build --chown=root:root --chmod=755 /opt/photoprism/ /opt/photoprism
@ -105,7 +102,7 @@ RUN echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80retries && \
echo 'APT::Get::Assume-Yes "true";' > /etc/apt/apt.conf.d/80forceyes && \
echo 'APT::Get::Fix-Missing "true";' > /etc/apt/apt.conf.d/80fixmissing && \
echo 'force-confold' > /etc/dpkg/dpkg.cfg.d/force-confold && \
apt-get update && apt-get -qq dist-upgrade && \
apt-get update && apt-get -qq upgrade && \
apt-get -qq install --no-install-recommends \
libc6 ca-certificates bash sudo nano avahi-utils jq lsof lshw \
exiftool mariadb-client sqlite3 tzdata gpg make zip unzip wget curl rsync \

View file

@ -56,7 +56,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_UPLOAD_NSFW="true" \
PHOTOPRISM_DETECT_NSFW="false" \
PHOTOPRISM_EXPERIMENTAL="false" \
PHOTOPRISM_SITE_URL="http://localhost:2342/" \
PHOTOPRISM_SITE_URL="http://photoprism.me:2342/" \
PHOTOPRISM_SITE_CAPTION="AI-Powered Photos App" \
PHOTOPRISM_SITE_DESCRIPTION="" \
PHOTOPRISM_SITE_AUTHOR="" \
@ -86,15 +86,14 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_WORKERS=0 \
PHOTOPRISM_WAKEUP_INTERVAL=900 \
PHOTOPRISM_AUTO_INDEX=300 \
PHOTOPRISM_AUTO_IMPORT=300 \
PHOTOPRISM_INIT="https"
PHOTOPRISM_AUTO_IMPORT=300
# Copy scripts.
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
# Update pre-installed packages.
RUN apt-get update && \
apt-get -qq dist-upgrade && \
apt-get -qq upgrade && \
/scripts/cleanup.sh
# Default working directory.

View file

@ -56,7 +56,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_UPLOAD_NSFW="true" \
PHOTOPRISM_DETECT_NSFW="false" \
PHOTOPRISM_EXPERIMENTAL="false" \
PHOTOPRISM_SITE_URL="http://localhost:2342/" \
PHOTOPRISM_SITE_URL="http://photoprism.me:2342/" \
PHOTOPRISM_SITE_CAPTION="AI-Powered Photos App" \
PHOTOPRISM_SITE_DESCRIPTION="" \
PHOTOPRISM_SITE_AUTHOR="" \
@ -86,8 +86,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_WORKERS=0 \
PHOTOPRISM_WAKEUP_INTERVAL=900 \
PHOTOPRISM_AUTO_INDEX=300 \
PHOTOPRISM_AUTO_IMPORT=300 \
PHOTOPRISM_INIT="https"
PHOTOPRISM_AUTO_IMPORT=300
# Copy scripts.
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/

View file

@ -59,7 +59,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_UPLOAD_NSFW="true" \
PHOTOPRISM_DETECT_NSFW="false" \
PHOTOPRISM_EXPERIMENTAL="false" \
PHOTOPRISM_SITE_URL="http://localhost:2342/" \
PHOTOPRISM_SITE_URL="http://photoprism.me:2342/" \
PHOTOPRISM_SITE_CAPTION="AI-Powered Photos App" \
PHOTOPRISM_SITE_DESCRIPTION="" \
PHOTOPRISM_SITE_AUTHOR="" \
@ -89,8 +89,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_WORKERS=0 \
PHOTOPRISM_WAKEUP_INTERVAL=900 \
PHOTOPRISM_AUTO_INDEX=300 \
PHOTOPRISM_AUTO_IMPORT=300 \
PHOTOPRISM_INIT="https"
PHOTOPRISM_AUTO_IMPORT=300
# Copy dist files, scripts, and debian backports sources list.
COPY --from=build --chown=root:root --chmod=755 /opt/photoprism/ /opt/photoprism

View file

@ -59,7 +59,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_UPLOAD_NSFW="true" \
PHOTOPRISM_DETECT_NSFW="false" \
PHOTOPRISM_EXPERIMENTAL="false" \
PHOTOPRISM_SITE_URL="http://localhost:2342/" \
PHOTOPRISM_SITE_URL="http://photoprism.me:2342/" \
PHOTOPRISM_SITE_CAPTION="AI-Powered Photos App" \
PHOTOPRISM_SITE_DESCRIPTION="" \
PHOTOPRISM_SITE_AUTHOR="" \
@ -89,8 +89,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_WORKERS=0 \
PHOTOPRISM_WAKEUP_INTERVAL=900 \
PHOTOPRISM_AUTO_INDEX=300 \
PHOTOPRISM_AUTO_IMPORT=300 \
PHOTOPRISM_INIT="https"
PHOTOPRISM_AUTO_IMPORT=300
# Copy dist files and scripts.
COPY --from=build --chown=root:root --chmod=755 /opt/photoprism/ /opt/photoprism

View file

@ -57,7 +57,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_UPLOAD_NSFW="true" \
PHOTOPRISM_DETECT_NSFW="false" \
PHOTOPRISM_EXPERIMENTAL="false" \
PHOTOPRISM_SITE_URL="http://localhost:2342/" \
PHOTOPRISM_SITE_URL="http://photoprism.me:2342/" \
PHOTOPRISM_SITE_CAPTION="AI-Powered Photos App" \
PHOTOPRISM_SITE_DESCRIPTION="" \
PHOTOPRISM_SITE_AUTHOR="" \
@ -87,15 +87,14 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_WORKERS=0 \
PHOTOPRISM_WAKEUP_INTERVAL=900 \
PHOTOPRISM_AUTO_INDEX=300 \
PHOTOPRISM_AUTO_IMPORT=300 \
PHOTOPRISM_INIT="https"
PHOTOPRISM_AUTO_IMPORT=300
# Copy scripts.
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
# Update pre-installed packages.
RUN apt-get update && \
apt-get -qq dist-upgrade && \
apt-get -qq upgrade && \
/scripts/cleanup.sh
# Default working directory.

View file

@ -57,7 +57,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_UPLOAD_NSFW="true" \
PHOTOPRISM_DETECT_NSFW="false" \
PHOTOPRISM_EXPERIMENTAL="false" \
PHOTOPRISM_SITE_URL="http://localhost:2342/" \
PHOTOPRISM_SITE_URL="http://photoprism.me:2342/" \
PHOTOPRISM_SITE_CAPTION="AI-Powered Photos App" \
PHOTOPRISM_SITE_DESCRIPTION="" \
PHOTOPRISM_SITE_AUTHOR="" \
@ -87,15 +87,14 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_WORKERS=0 \
PHOTOPRISM_WAKEUP_INTERVAL=900 \
PHOTOPRISM_AUTO_INDEX=300 \
PHOTOPRISM_AUTO_IMPORT=300 \
PHOTOPRISM_INIT="https"
PHOTOPRISM_AUTO_IMPORT=300
# Copy scripts.
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
# Update pre-installed packages.
RUN apt-get update && \
apt-get -qq dist-upgrade && \
apt-get -qq upgrade && \
/scripts/cleanup.sh
# Default working directory.

View file

@ -1,114 +0,0 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/develop:mantic as build
# Copyright © 2018 - 2023 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/
# Declare build parameters.
ARG TARGETARCH
ARG TARGETPLATFORM
ARG BUILD_TAG
# Copy source to image.
WORKDIR "/go/src/github.com/photoprism/photoprism"
COPY . .
# Build app.
RUN make all install DESTDIR=/opt/photoprism
################################################## PRODUCTION STAGE ####################################################
#### Base Image: Ubuntu 23.10 (Mantic Minotaur)
FROM photoprism/develop:mantic-slim
# Add Open Container Initiative (OCI) annotations.
# See: https://github.com/opencontainers/image-spec/blob/main/annotations.md
LABEL org.opencontainers.image.title="PhotoPrism® (Ubuntu 23.10)"
LABEL org.opencontainers.image.description="Ubuntu 23.10 (Mantic Minotaur)"
LABEL org.opencontainers.image.url="https://hub.docker.com/r/photoprism/photoprism"
LABEL org.opencontainers.image.source="https://github.com/photoprism/photoprism"
LABEL org.opencontainers.image.documentation="https://docs.photoprism.app/getting-started/"
LABEL org.opencontainers.image.authors="PhotoPrism UG <hello@photoprism.app>"
LABEL org.opencontainers.image.vendor="PhotoPrism UG"
# Declare build parameters.
ARG TARGETARCH
ARG BUILD_TAG
# Set environment variables, see https://docs.photoprism.app/getting-started/config-options/.
ENV PHOTOPRISM_ARCH=$TARGETARCH \
DOCKER_TAG=$BUILD_TAG \
DOCKER_ENV="prod" \
TMPDIR="/tmp" \
DEBIAN_FRONTEND="noninteractive" \
TF_CPP_MIN_LOG_LEVEL="2" \
PROG="photoprism" \
PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \
PHOTOPRISM_IMPORT_PATH="/photoprism/import" \
PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \
PHOTOPRISM_STORAGE_PATH="/photoprism/storage" \
PHOTOPRISM_BACKUP_PATH="/photoprism/storage/backups" \
PHOTOPRISM_LOG_FILENAME="/photoprism/storage/photoprism.log" \
PHOTOPRISM_PID_FILENAME="/photoprism/storage/photoprism.pid" \
PHOTOPRISM_DEBUG="false" \
PHOTOPRISM_PUBLIC="false" \
PHOTOPRISM_READONLY="false" \
PHOTOPRISM_UPLOAD_NSFW="true" \
PHOTOPRISM_DETECT_NSFW="false" \
PHOTOPRISM_EXPERIMENTAL="false" \
PHOTOPRISM_SITE_URL="http://localhost:2342/" \
PHOTOPRISM_SITE_CAPTION="AI-Powered Photos App" \
PHOTOPRISM_SITE_DESCRIPTION="" \
PHOTOPRISM_SITE_AUTHOR="" \
PHOTOPRISM_HTTP_HOST="0.0.0.0" \
PHOTOPRISM_HTTP_PORT=2342 \
PHOTOPRISM_DATABASE_DRIVER="sqlite" \
PHOTOPRISM_DATABASE_SERVER="" \
PHOTOPRISM_DATABASE_NAME="photoprism" \
PHOTOPRISM_DATABASE_USER="photoprism" \
PHOTOPRISM_DATABASE_PASSWORD="" \
PHOTOPRISM_DISABLE_CHOWN="false" \
PHOTOPRISM_DISABLE_WEBDAV="false" \
PHOTOPRISM_DISABLE_SETTINGS="false" \
PHOTOPRISM_DISABLE_BACKUPS="false" \
PHOTOPRISM_DISABLE_EXIFTOOL="false" \
PHOTOPRISM_DISABLE_PLACES="false" \
PHOTOPRISM_DISABLE_TENSORFLOW="false" \
PHOTOPRISM_DISABLE_FACES="false" \
PHOTOPRISM_DISABLE_CLASSIFICATION="false" \
PHOTOPRISM_RAW_PRESETS="false" \
PHOTOPRISM_THUMB_FILTER="lanczos" \
PHOTOPRISM_THUMB_UNCACHED="false" \
PHOTOPRISM_THUMB_SIZE=2048 \
PHOTOPRISM_THUMB_SIZE_UNCACHED=7680 \
PHOTOPRISM_JPEG_SIZE=7680 \
PHOTOPRISM_JPEG_QUALITY=85 \
PHOTOPRISM_WORKERS=0 \
PHOTOPRISM_WAKEUP_INTERVAL=900 \
PHOTOPRISM_AUTO_INDEX=300 \
PHOTOPRISM_AUTO_IMPORT=300 \
PHOTOPRISM_INIT="https"
# Copy scripts.
COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/
# Update pre-installed packages.
RUN apt-get update && \
apt-get -qq dist-upgrade && \
/scripts/cleanup.sh
# Default working directory.
WORKDIR /photoprism
# Expose HTTP(S) ports.
EXPOSE 2342 2443
# Copy app files.
COPY --from=build --chown=root:root --chmod=755 /opt/photoprism/ /opt/photoprism
# Declare container entrypoint script.
ENTRYPOINT ["/scripts/entrypoint.sh"]
# Start app.
CMD ["/opt/photoprism/bin/photoprism", "start"]

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "photoprism",
"version": "1",
"version": "1.0.0",
"description": "AI-Powered Photos App",
"author": "PhotoPrism UG",
"license": "AGPL-3.0",
@ -20,45 +20,45 @@
"gettext-compile": "gettext-compile --output src/locales/translations.json src/locales/*.po"
},
"dependencies": {
"@babel/cli": "^7.23.0",
"@babel/core": "^7.23.2",
"@babel/eslint-parser": "^7.22.15",
"@babel/cli": "^7.21.5",
"@babel/core": "^7.21.8",
"@babel/eslint-parser": "^7.21.8",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
"@babel/plugin-transform-runtime": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"@babel/register": "^7.22.15",
"@babel/runtime": "^7.23.2",
"@babel/plugin-transform-runtime": "^7.21.4",
"@babel/preset-env": "^7.21.5",
"@babel/register": "^7.21.0",
"@babel/runtime": "^7.21.5",
"@lcdp/offline-plugin": "^5.1.1",
"@vvo/tzdb": "^6.109.0",
"axios": "^1.6.0",
"axios-mock-adapter": "^1.22.0",
"babel-loader": "^9.1.3",
"@vvo/tzdb": "^6.107.0",
"axios": "^1.4.0",
"axios-mock-adapter": "^1.21.4",
"babel-loader": "^9.1.2",
"babel-plugin-istanbul": "^6.1.1",
"browserslist": "^4.22.1",
"chai": "^4.3.10",
"browserslist": "^4.21.5",
"chai": "^4.3.7",
"cheerio": "1.0.0-rc.10",
"chrome-finder": "^1.0.7",
"core-js": "^3.33.1",
"core-js": "^3.30.2",
"cross-env": "^7.0.3",
"css-loader": "^6.8.1",
"cssnano": "^6.0.1",
"css-loader": "^6.7.3",
"cssnano": "^5.1.14",
"easygettext": "^2.17.0",
"eslint": "^8.51.0",
"eslint-config-prettier": "^9.0.0",
"eslint-config-standard": "^17.1.0",
"eslint": "^8.40.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-standard": "^17.0.0",
"eslint-formatter-pretty": "^5.0.0",
"eslint-plugin-html": "^7.1.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier-vue": "^5.0.0",
"eslint-plugin-prettier-vue": "^4.2.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.17.0",
"eslint-plugin-vue": "^9.13.0",
"eslint-webpack-plugin": "^4.0.1",
"eventsource-polyfill": "^0.9.6",
"file-loader": "^6.2.0",
"file-saver": "^2.0.5",
"hls.js": "^1.4.12",
"hls.js": "^1.2.9",
"i": "^0.3.7",
"karma": "^6.4.2",
"karma-chrome-launcher": "^3.2.0",
@ -67,31 +67,32 @@
"karma-mocha": "^2.0.1",
"karma-verbose-reporter": "^0.0.8",
"karma-webpack": "^5.0.0",
"luxon": "^3.4.3",
"maplibre-gl": "^3.5.1",
"luxon": "^3.3.0",
"maplibre-gl": "^2.4.0",
"memoize-one": "^6.0.0",
"mini-css-extract-plugin": "^2.7.6",
"mini-css-extract-plugin": "^2.7.2",
"minimist": ">=1.2.5",
"mocha": "^10.2.0",
"node-storage-shim": "^2.0.1",
"photoswipe": "^4.1.3",
"postcss": "^8.4.31",
"plyr": "^3.7.8",
"postcss": "^8.4.20",
"postcss-import": "^15.1.0",
"postcss-loader": "^7.3.3",
"postcss-preset-env": "^9.2.0",
"postcss-loader": "^7.0.2",
"postcss-preset-env": "^7.8.3",
"postcss-reporter": "^7.0.5",
"postcss-url": "^10.1.3",
"prettier": "^3.0.3",
"prettier": "^2.8.8",
"pubsub-js": "^1.9.4",
"regenerator-runtime": "^0.14.0",
"regenerator-runtime": "^0.13.11",
"resolve-url-loader": "^5.0.0",
"sass": "^1.69.4",
"sass-loader": "^13.3.2",
"sass": "^1.62.1",
"sass-loader": "^13.2.0",
"server": "^1.0.38",
"sockette": "^2.0.6",
"style-loader": "^3.3.3",
"style-loader": "^3.3.1",
"svg-url-loader": "^8.0.0",
"tar": "^6.2.0",
"tar": "^6.1.13",
"url-loader": "^4.1.1",
"util": "^0.12.5",
"vue": "^2.7.14",
@ -106,17 +107,17 @@
"vue-template-compiler": "^2.7.13",
"vue2-filters": "^0.14.0",
"vuetify": "^1.5.24",
"webpack": "^5.89.0",
"webpack-bundle-analyzer": "^4.9.1",
"webpack-cli": "^5.1.4",
"webpack-hot-middleware": "^2.25.4",
"webpack": "^5.83.0",
"webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^5.1.1",
"webpack-hot-middleware": "^2.25.3",
"webpack-manifest-plugin": "^5.0.0",
"webpack-md5-hash": "^0.0.6",
"webpack-merge": "^5.10.0"
"webpack-merge": "^5.8.0"
},
"engines": {
"node": ">= 18.0.0",
"npm": ">= 9.0.0",
"node": ">= 16.0.0",
"npm": ">= 8.0.0",
"yarn": "please use npm"
},
"browserslist": ">0.25% and last 2 years"

View file

@ -164,9 +164,9 @@ export default [
},
{
name: "moment",
path: "/moments/:album/:slug",
path: "/moments/:uid/:slug",
component: AlbumPhotos,
meta: { collectionTitle: "Moments", collectionRoute: "moments", auth: true },
meta: { collName: "Moments", collRoute: "moments", auth: true },
},
{
name: "albums",
@ -177,9 +177,9 @@ export default [
},
{
name: "album",
path: "/albums/:album/:slug",
path: "/albums/:uid/:slug",
component: AlbumPhotos,
meta: { collectionTitle: "Albums", collectionRoute: "albums", auth: true },
meta: { collName: "Albums", collRoute: "albums", auth: true },
},
{
name: "calendar",
@ -190,9 +190,9 @@ export default [
},
{
name: "month",
path: "/calendar/:album/:slug",
path: "/calendar/:uid/:slug",
component: AlbumPhotos,
meta: { collectionTitle: "Calendar", collectionRoute: "calendar", auth: true },
meta: { collName: "Calendar", collRoute: "calendar", auth: true },
},
{
name: "folders",
@ -203,9 +203,9 @@ export default [
},
{
name: "folder",
path: "/folders/:album/:slug",
path: "/folders/:uid/:slug",
component: AlbumPhotos,
meta: { collectionTitle: "Folders", collectionRoute: "folders", auth: true },
meta: { collName: "Folders", collRoute: "folders", auth: true },
},
{
name: "unsorted",
@ -263,25 +263,16 @@ export default [
meta: { title: $gettext("Places"), auth: true },
},
{
name: "places_view",
path: "/places/view/:s",
name: "places_query",
path: "/places/:q",
component: Places,
meta: { title: $gettext("Places"), auth: true },
},
{
name: "places_browse",
path: "/places/browse",
component: Photos,
name: "places_scope",
path: "/places/:s/:q",
component: Places,
meta: { title: $gettext("Places"), auth: true },
beforeEnter: (to, from, next) => {
if (session.loginRequired()) {
next({ name: "login" });
} else if (config.deny("photos", "search")) {
next({ name: "albums" });
} else {
next();
}
},
},
{
name: "states",
@ -292,9 +283,9 @@ export default [
},
{
name: "state",
path: "/states/:album/:slug",
path: "/states/:uid/:slug",
component: AlbumPhotos,
meta: { collectionTitle: "Places", collectionRoute: "states", auth: true },
meta: { collName: "Places", collRoute: "states", auth: true },
},
{
name: "files",

View file

@ -327,12 +327,6 @@ export default class Config {
case "files":
this.values.count.files += data.count;
break;
case "hidden":
this.values.count.hidden += data.count;
break;
case "archived":
this.values.count.archived += data.count;
break;
case "favorites":
this.values.count.favorites += data.count;
break;

View file

@ -84,14 +84,11 @@ export default class Session {
this.config.progress(80);
this.redeemToken(shared.token).finally(() => {
this.config.progress(99);
// Redirect URL.
const location = shared.uri ? shared.uri : this.config.baseUri + "/";
// Redirect to URL after one second.
setTimeout(() => {
window.location = location;
}, 1000);
if (shared.uri) {
window.location = shared.uri;
} else {
window.location = this.config.baseUri + "/";
}
});
} else {
this.config.progress(80);
@ -103,7 +100,7 @@ export default class Session {
}
useSessionStorage() {
this.reset();
this.deleteId();
this.storage.setItem(this.storage_key, "true");
this.storage = window.sessionStorage;
}
@ -115,7 +112,7 @@ export default class Session {
applyId(id) {
if (!id) {
this.reset();
this.deleteId();
return false;
}
@ -149,6 +146,8 @@ export default class Session {
this.storage.removeItem("session_id");
delete Api.defaults.headers.common[SessionHeader];
this.deleteAll();
}
setResp(resp) {
@ -273,17 +272,9 @@ export default class Session {
this.storage.removeItem("user");
}
deleteClipboard() {
this.storage.removeItem("clipboard");
this.storage.removeItem("photo_clipboard");
this.storage.removeItem("album_clipboard");
}
reset() {
this.deleteId();
deleteAll() {
this.deleteData();
this.deleteUser();
this.deleteClipboard();
}
sendClientInfo() {
@ -312,20 +303,16 @@ export default class Session {
}
login(username, password, token) {
this.reset();
this.deleteId();
return Api.post("session", { username, password, token }).then((resp) => {
const reload = this.config.getLanguage() !== resp.data?.config?.settings?.ui?.language;
this.setResp(resp);
this.onLogin();
this.sendClientInfo();
return Promise.resolve(reload);
});
}
onLogin() {
this.sendClientInfo();
}
refresh() {
// Refresh session information.
if (this.config.isPublic()) {
@ -343,7 +330,7 @@ export default class Session {
return Promise.resolve();
})
.catch(() => {
this.reset();
this.deleteId();
if (!this.isLogin()) {
window.location.reload();
}
@ -367,7 +354,7 @@ export default class Session {
}
onLogout(noRedirect) {
this.reset();
this.deleteId();
if (noRedirect !== true && !this.isLogin()) {
window.location = this.config.baseUri + "/";
}

View file

@ -153,14 +153,6 @@ export default class Util {
return s.replace(/\w\S*/g, (w) => w.replace(/^\w/, (c) => c.toUpperCase()));
}
static ucFirst(s) {
if (!s || s === "") {
return "";
}
return s.charAt(0).toUpperCase() + s.slice(1);
}
static generateToken() {
return (Math.random() + 1).toString(36).substring(6);
}
@ -179,7 +171,6 @@ export default class Util {
return "Unprocessed Sensor Data (RAW)";
case "mov":
case "qt":
case "qt ":
return "Apple QuickTime";
case "bmp":
return "Bitmap";
@ -202,9 +193,8 @@ export default class Util {
return "AOMedia Video 1 (AV1)";
case "avifs":
return "AVIF Image Sequence";
case "hvc":
case "hevc":
case "hev1":
case "hvc":
case "hvc1":
return "High Efficiency Video Coding (HEVC) / H.265";
case "m4v":
@ -245,31 +235,6 @@ export default class Util {
}
}
static formatCodec(codec) {
if (!codec) {
return "";
}
switch (codec) {
case "webp":
case "extended webp":
return "WebP";
case "webm":
return "WebM";
case "av1c":
case "av01":
return "AV1";
case "avc1":
return "AVC";
case "hvc":
case "hev1":
case "hvc1":
return "HEVC";
default:
return codec.toUpperCase();
}
}
static codecName(value) {
if (!value || typeof value !== "string") {
return "";
@ -280,19 +245,16 @@ export default class Util {
return "Unprocessed Sensor Data (RAW)";
case "mov":
case "qt":
case "qt ":
return "Apple QuickTime (MOV)";
case "avc":
case "avc1":
return "Advanced Video Coding (AVC) / H.264";
case "hvc":
case "hevc":
case "hev1":
case "hvc":
case "hvc1":
return "High Efficiency Video Coding (HEVC) / H.265";
case "vvc":
return "Versatile Video Coding (VVC) / H.266";
case "av1c":
case "av01":
return "AOMedia Video 1 (AV1)";
case "gif":
@ -301,8 +263,6 @@ export default class Util {
return "Matroska Multimedia Container (MKV)";
case "webp":
return "Google WebP";
case "extended webp":
return "Extended WebP";
case "webm":
return "Google WebM";
case "mpeg":

View file

@ -5,8 +5,8 @@
<v-toolbar flat :dense="$vuetify.breakpoint.smAndDown" class="page-toolbar" color="secondary">
<v-toolbar-title :title="album.Title">
<span class="hidden-xs-only">
<router-link :to="{ name: collectionRoute }">
{{ T(collectionTitle) }}
<router-link :to="{ name: collRoute }">
{{ T(collName) }}
</router-link>
<v-icon>{{ navIcon }}</v-icon>
</span>
@ -33,12 +33,10 @@
<v-icon>get_app</v-icon>
</v-btn>
<v-btn v-if="settings.view === 'cards'" icon class="action-view-list" :title="$gettext('Toggle View')"
@click.stop="setView('list')">
<v-btn v-if="settings.view === 'cards'" icon class="action-view-list" :title="$gettext('Toggle View')" @click.stop="setView('list')">
<v-icon>view_list</v-icon>
</v-btn>
<v-btn v-else-if="settings.view === 'list'" icon class="action-view-mosaic" :title="$gettext('Toggle View')"
@click.stop="setView('mosaic')">
<v-btn v-else-if="settings.view === 'list'" icon class="action-view-mosaic" :title="$gettext('Toggle View')" @click.stop="setView('mosaic')">
<v-icon>view_comfy</v-icon>
</v-btn>
<v-btn v-else icon class="action-view-cards" :title="$gettext('Toggle View')" @click.stop="setView('cards')">
@ -70,8 +68,7 @@
<p-share-dialog :show="dialog.share" :model="album" @upload="webdavUpload"
@close="dialog.share = false"></p-share-dialog>
<p-share-upload-dialog :show="dialog.upload" :items="{albums: album.getId()}" :model="album"
@cancel="dialog.upload = false"
<p-share-upload-dialog :show="dialog.upload" :items="{albums: album.getId()}" :model="album" @cancel="dialog.upload = false"
@confirm="dialog.upload = false"></p-share-upload-dialog>
<p-album-edit-dialog :show="dialog.edit" :album="album" @close="dialog.edit = false"></p-album-edit-dialog>
</v-form>
@ -80,40 +77,34 @@
import Event from "pubsub-js";
import Notify from "common/notify";
import download from "common/download";
import {T} from "common/vm";
import { T } from "common/vm";
export default {
name: 'PAlbumToolbar',
props: {
album: {
type: Object,
default: () => {
},
default: () => {},
},
filter: {
type: Object,
default: () => {
},
default: () => {},
},
updateFilter: {
type: Function,
default: () => {
},
default: () => {},
},
updateQuery: {
type: Function,
default: () => {
},
default: () => {},
},
settings: {
type: Object,
default: () => {
},
default: () => {},
},
refresh: {
type: Function,
default: () => {
},
default: () => {},
},
},
data() {
@ -134,8 +125,8 @@ export default {
experimental: this.$config.get("experimental"),
isFullScreen: !!document.fullscreenElement,
categories: this.$config.albumCategories(),
collectionTitle: this.$route.meta?.collectionTitle ? this.$route.meta.collectionTitle : this.$gettext("Albums"),
collectionRoute: this.$route.meta?.collectionRoute ? this.$route.meta.collectionRoute : "albums",
collName: this.$route.meta && this.$route.meta.collName ? this.$route.meta.collName : this.$gettext("Albums"),
collRoute: this.$route.meta && this.$route.meta.collRoute ? this.$route.meta.collRoute : "albums",
navIcon: this.$rtl ? 'navigate_before' : 'navigate_next',
searchExpanded: false,
options: {
@ -176,12 +167,7 @@ export default {
this.dialog.upload = true;
},
showUpload() {
// Pre-select manually managed albums in upload dialog.
if(this.album.Type === "album") {
Event.publish("dialog.upload", {albums: [this.album]});
} else {
Event.publish("dialog.upload", {albums: []});
}
Event.publish("dialog.upload");
},
expand() {
this.searchExpanded = !this.searchExpanded;

View file

@ -1,23 +1,28 @@
<template>
<div class="auth-footer">
<footer>
<footer v-if="sponsor">
<v-layout wrap align-top pa-0 ma-0>
<v-flex xs12 sm6 class="pa-0 body-2 text-selectable text-xs-center white--text text-sm-left">
{{ about }}
{{ $config.getAbout() }}
</v-flex>
<v-flex v-if="legalInfo" xs12 sm6 class="pa-0 body-2 text-xs-center text-sm-right white--text">
<a v-if="legalUrl" :href="legalUrl" target="_blank" class="text-link"
:style="`color: ${colors.link}!important`">{{ legalInfo }}</a>
<span v-else>{{ legalInfo }}</span>
<v-flex v-if="config.legalInfo" xs12 sm6 class="pa-0 body-2 text-xs-center text-sm-right white--text">
<a v-if="config.legalUrl" :href="config.legalUrl" target="_blank" class="text-link"
:style="`color: ${colors.link}!important`">{{ config.legalInfo }}</a>
<span v-else>{{ config.legalInfo }}</span>
</v-flex>
<v-flex v-else-if="caption" xs12 sm6
class="pa-0 body-2 text-selectable text-xs-center text-sm-right white--text">
<strong>{{ caption }}</strong>
<v-flex v-else xs12 class="pa-0 body-2 text-selectable text-xs-center white--text text-sm-right sm6">
<strong>{{ config.siteCaption ? config.siteCaption : config.siteTitle }}</strong>
</v-flex>
<v-flex v-else xs12 sm6 class="pa-0 body-2 text-selectable text-xs-center text-sm-right white--text">
<router-link to="/about" class="text-link"><span class="white--text">Made with in Berlin</span>
</router-link>
</v-layout>
</footer>
<footer v-else>
<v-layout wrap align-top pa-0 ma-0>
<v-flex xs12 sm6 class="pa-0 body-2 text-xs-center text-sm-left white--text text-selectable">
<strong>{{ config.siteTitle }}</strong> {{ config.siteCaption }}
</v-flex>
<v-flex xs12 sm6 class="pa-0 body-2 text-selectable text-xs-center text-sm-right">
<router-link to="/about" class="text-link"><span class="white--text">Made with in Berlin</span></router-link>
</v-flex>
</v-layout>
</footer>
@ -41,14 +46,9 @@ export default {
},
},
data() {
const config = this.$config;
return {
about: config.getAbout(),
sponsor: config.isSponsor(),
caption: config.values.siteCaption ? config.values.siteCaption : config.values.siteTitle,
legalUrl: config.values.legalUrl,
legalInfo: config.values.legalInfo,
config: config.values,
sponsor: this.$config.isSponsor(),
config: this.$config.values,
rtl: this.$rtl,
};
},

View file

@ -33,7 +33,6 @@ import PPhotoToolbar from "component/photo/toolbar.vue";
import PPhotoCards from "component/photo/cards.vue";
import PPhotoMosaic from "component/photo/mosaic.vue";
import PPhotoList from "component/photo/list.vue";
import PPhotoPreview from "component/photo/preview.vue";
import PPhotoClipboard from "component/photo/clipboard.vue";
import PAlbumClipboard from "component/album/clipboard.vue";
import PAlbumToolbar from "component/album/toolbar.vue";
@ -60,7 +59,6 @@ components.install = (Vue) => {
Vue.component("PPhotoCards", PPhotoCards);
Vue.component("PPhotoMosaic", PPhotoMosaic);
Vue.component("PPhotoList", PPhotoList);
Vue.component("PPhotoPreview", PPhotoPreview);
Vue.component("PPhotoClipboard", PPhotoClipboard);
Vue.component("PAlbumClipboard", PAlbumClipboard);
Vue.component("PAlbumToolbar", PAlbumToolbar);

View file

@ -65,7 +65,7 @@
</v-list>
</v-toolbar>
<v-list class="pt-3 p-flex-menu navigation-menu">
<v-list class="pt-3 p-flex-menu">
<v-list-tile v-if="isMini && !isRestricted" class="nav-expand" @click.stop="toggleIsMini()">
<v-list-tile-action :title="$gettext('Expand')">
<v-icon v-if="!rtl">chevron_right</v-icon>
@ -165,8 +165,6 @@
<v-list-tile-content>
<v-list-tile-title :class="`p-flex-menuitem menu-item ${rtl ? '--rtl' : ''}`">
<translate>Archive</translate>
<span v-show="config.count.archived > 0"
:class="`nav-count ${rtl ? '--rtl' : ''}`">{{ config.count.archived | abbreviateCount }}</span>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
@ -524,18 +522,6 @@
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile v-show="featMembership" :to="{ name: 'upgrade' }" class="nav-membership" @click.stop="">
<v-list-tile-action :title="$gettext('Support Our Mission')">
<v-icon>diamond</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>
<translate key="Support Our Mission">Support Our Mission</translate>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
<v-list class="p-user-box">
@ -553,7 +539,7 @@
</v-list-tile-content>
</v-list-tile>
<v-list-tile v-show="auth && !isPublic" class="p-profile" @click.stop="onAccountSettings">
<v-list-tile v-show="auth && !isPublic && $config.feature('account')" class="p-profile" @click.stop="onAccount">
<v-list-tile-avatar size="36">
<img :src="userAvatarURL" :alt="accountInfo" :title="accountInfo">
</v-list-tile-avatar>
@ -678,7 +664,7 @@
<span v-else>{{ config.legalInfo }}</span>
</div>
<p-reload-dialog :show="reload.dialog" @close="reload.dialog = false"></p-reload-dialog>
<p-upload-dialog :show="upload.dialog" :data="upload.data" @cancel="upload.dialog = false"
<p-upload-dialog :show="upload.dialog" @cancel="upload.dialog = false"
@confirm="upload.dialog = false"></p-upload-dialog>
<p-photo-edit-dialog :show="edit.dialog" :selection="edit.selection" :index="edit.index" :album="edit.album"
@close="edit.dialog = false"></p-photo-edit-dialog>
@ -687,7 +673,6 @@
<script>
import Event from "pubsub-js";
import Album from "model/album";
export default {
name: "PNavigation",
@ -717,7 +702,6 @@ export default {
const isReadOnly = this.$config.get("readonly");
const isRestricted = this.$config.deny("photos", "access_library");
const isSuperAdmin = this.$session.isSuperAdmin();
const tier = this.$config.getTier();
return {
canSearchPlaces: this.$config.allow("places", "search"),
@ -731,8 +715,7 @@ export default {
appIcon: this.$config.getIcon(),
indexing: false,
drawer: null,
featUpgrade: tier < 6 && isSuperAdmin && !isPublic && !isDemo,
featMembership: tier < 3 && isSuperAdmin && !isPublic && !isDemo,
featUpgrade: this.$config.getTier() < 8 && isSuperAdmin && !isPublic && !isDemo,
isRestricted: isRestricted,
isMini: localStorage.getItem('last_navigation_mode') !== 'false' || isRestricted,
isDemo: isDemo,
@ -751,7 +734,6 @@ export default {
},
upload: {
dialog: false,
data: {},
},
edit: {
dialog: false,
@ -795,14 +777,7 @@ export default {
this.subscriptions.push(Event.subscribe('index', this.onIndex));
this.subscriptions.push(Event.subscribe('import', this.onIndex));
this.subscriptions.push(Event.subscribe("dialog.reload", () => this.reload.dialog = true));
this.subscriptions.push(Event.subscribe("dialog.upload", (ev, data) => {
if(data) {
this.upload.data = data;
} else {
this.upload.data = {};
}
this.upload.dialog = true;
}));
this.subscriptions.push(Event.subscribe("dialog.upload", () => this.upload.dialog = true));
this.subscriptions.push(Event.subscribe("dialog.edit", (ev, data) => {
if (!this.edit.dialog) {
this.edit.dialog = true;
@ -832,18 +807,7 @@ export default {
},
openUpload() {
if (this.auth && !this.isReadOnly && this.$config.feature('upload')) {
if (this.$route.name === 'album' && this.$route.params?.album) {
return new Album().find(this.$route.params?.album).then(m => {
this.upload.dialog = true;
this.upload.data = {albums: [m]};
}).catch(() => {
this.upload.dialog = true;
this.upload.data = {albums: []};
});
} else {
this.upload.dialog = true;
this.upload.data = {albums: []};
}
this.upload.dialog = true;
} else {
this.goHome();
}
@ -863,12 +827,8 @@ export default {
this.isMini = !this.isMini;
localStorage.setItem('last_navigation_mode', `${this.isMini}`);
},
onAccountSettings: function () {
if (this.$config.feature('account')) {
this.$router.push({name: "settings_account"});
} else {
this.$router.push({name: "settings"});
}
onAccount: function () {
this.$router.push({name: "settings_account"});
},
onInfo() {
if (this.isSponsor && this.config.legalUrl) {

View file

@ -76,9 +76,7 @@
</div>
</div>
<div v-if="player.show" class="video-viewer" @click.stop.prevent="closePlayer" @keydown.esc.stop.prevent="closePlayer">
<p-video-player ref="player" :source="player.source" :poster="player.poster"
:height="player.height" :width="player.width" :autoplay="player.autoplay" :loop="player.loop" @close="closePlayer">
</p-video-player>
<p-video-player ref="player" :videos="videos" :index="0" @close="closePlayer"></p-video-player>
</div>
</div>
</template>
@ -101,13 +99,14 @@ export default {
canDownload: this.$config.allow("photos", "download") && this.$config.feature("download"),
selection: this.$clipboard.selection,
config: this.$config.values,
item: new Thumb(),
item: new Thumb(false),
subscriptions: [],
interval: false,
slideshow: {
active: false,
next: 0,
},
videos: [],
player: {
show: false,
loop: false,
@ -175,28 +174,23 @@ export default {
},
onPlay() {
if (this.item && this.item.Playable) {
new Photo().find(this.item.UID).then((video) => this.openPlayer(video));
new Photo().find(this.item.UID).then((photo) => this.openPlayer(photo));
}
},
openPlayer(video) {
if (!video) {
openPlayer(photo) {
if (!photo) {
this.$notify.error(this.$gettext("No video selected"));
return;
}
const params = video.videoParams();
const video = photo.video();
if (params.error) {
this.$notify.error(params.error);
if (!video || video.Error) {
this.$notify.error(this.$gettext("Not Found"));
return;
}
// Set video parameters.
this.player.loop = params.loop;
this.player.width = params.width;
this.player.height = params.height;
this.player.poster = params.poster;
this.player.source = params.uri;
this.videos = [video];
// Play video.
this.player.show = true;

View file

@ -31,7 +31,7 @@
ref="items"
:key="photo.ID"
:data-index="index"
class="flex xs12 sm6 md4 lg3 xlg2 ul1 d-flex"
class="flex xs12 sm6 md4 lg3 xlg2 xxxl1 d-flex"
>
<div v-if="index < firstVisibleElementIndex || index > lastVisibileElementIndex"
:data-uid="photo.UID"
@ -92,7 +92,7 @@
>
<v-layout v-if="photo.Type === 'live' || photo.Type === 'animated'" class="live-player">
<video :id="'live-player-' + photo.ID" :key="photo.ID" width="500" height="500" preload="none"
loop muted playsinline>
loop muted playsinline>
<source :src="photo.videoUrl()">
</video>
</v-layout>
@ -203,11 +203,6 @@
<i>movie</i>
{{ photo.getVideoInfo() }}
</button>
<button v-else-if="photo.Type === 'live'" :title="$gettext('Live')"
@click.exact="openPhoto(index)">
<i>play_circle</i>
{{ photo.getVideoInfo() }}
</button>
<button v-else-if="photo.Type === 'animated'" :title="$gettext('Animated')+' GIF'"
@click.exact="openPhoto(index)">
<i>gif_box</i>
@ -223,11 +218,6 @@
<i>photo_camera</i>
{{ photo.getPhotoInfo() }}
</button>
<button v-if="photo.LensID > 1 || photo.FocalLength" :title="$gettext('Lens')" class="action-lens-edit"
:data-uid="photo.UID" @click.exact="editPhoto(index)">
<i>camera</i>
{{ photo.getLensInfo() }}
</button>
<template v-if="filter.order === 'name' && $config.feature('download')">
<br>
<button :title="$gettext('Name')"
@ -467,7 +457,7 @@ export default {
/**
* updating the clipboard does not rerender this component. Because of that
* there can be scenarios where the select-icon is missing after a change,
* for example when selecting multiple elements at once. We therefore
* for example when selecting mutliple elements at once. We therefore
* force an update to fix that.
*/
this.$forceUpdate();

View file

@ -163,10 +163,6 @@ import Photo from "model/photo";
export default {
name: 'PPhotoClipboard',
props: {
context: {
type: String,
default: 'photos',
},
selection: {
type: Array,
default: () => [],
@ -179,6 +175,10 @@ export default {
type: Object,
default: () => {},
},
context: {
type: String,
default: '',
},
},
data() {
const features = this.$config.settings().features;

View file

@ -1,6 +1,6 @@
<template>
<v-container grid-list-xs fluid class="pa-2 p-photos p-photo-mosaic">
<div v-if="photos.length === 0" class="pa-0">
<template v-if="photos.length === 0">
<v-alert
:value="true"
color="secondary-dark"
@ -24,7 +24,7 @@
</template>
</p>
</v-alert>
</div>
</template>
<v-layout row wrap class="search-results photo-results mosaic-view" :class="{'select-results': selectMode}">
<div
v-for="(photo, index) in photos"
@ -60,7 +60,7 @@
@mouseleave="pauseLive(photo)">
<v-layout v-if="photo.Type === 'live' || photo.Type === 'animated'" class="live-player">
<video :id="'live-player-' + photo.ID" :key="photo.ID" width="224" height="224" preload="none"
loop muted playsinline>
loop muted playsinline>
<source :src="photo.videoUrl()">
</video>
</v-layout>

View file

@ -1,49 +0,0 @@
<template>
<div class="p-photo-preview pa-0 ma-0 elevation-0 v-card v-sheet v-sheet--tile no-transition" :title="title">
<div class="v-responsive v-image card darken-1 elevation-0 clickable" @click.prevent.stop="openPhoto">
<div class="v-responsive__sizer" style="padding-bottom: 100%;"></div>
<div class="v-image__image v-image__image--cover" :style="cover"></div>
<div class="v-responsive__content"></div>
</div>
</div>
</template>
<script>
import Photo from "model/photo";
import Thumb from "model/thumb";
export default {
name: 'PPhotoPreview',
props: {
model: {
type: Object,
default: () => new Photo(false),
},
},
data() {
return {
url: this.model.thumbnailUrl('tile_500'),
title: this.model.Title ? this.model.Title : '',
};
},
computed: {
cover() {
return `background-image: url('${this.url}'); background-position: center center;`;
},
},
watch: {
model() {
this.url = this.model.thumbnailUrl('tile_500');
this.title = this.model.Title ? this.model.Title : '';
},
},
methods: {
openPhoto() {
if (!this.$viewer || !this.model) {
return;
}
this.$viewer.show(Thumb.fromFiles([this.model]), 0);
},
},
};
</script>

View file

@ -1,68 +1,46 @@
<template>
<v-form ref="form" lazy-validation
dense autocomplete="off" class="p-photo-toolbar" accept-charset="UTF-8"
:class="{'embedded': embedded}"
@submit.prevent="updateQuery()">
<v-toolbar flat :dense="$vuetify.breakpoint.smAndDown" :height="embedded ? 45 : undefined"
class="page-toolbar" color="secondary">
<template v-if="!embedded">
<v-text-field :value="filter.q"
class="input-search background-inherit elevation-0"
solo hide-details clearable overflow single-line validate-on-blur
autocorrect="off"
autocapitalize="none"
browser-autocomplete="off"
:label="$gettext('Search')"
prepend-inner-icon="search"
color="secondary-dark"
@change="(v) => {updateFilter({'q': v})}"
@keyup.enter.native="(e) => updateQuery({'q': e.target.value})"
@click:clear="() => {updateQuery({'q': ''})}"
></v-text-field>
<v-toolbar flat :dense="$vuetify.breakpoint.smAndDown" class="page-toolbar" color="secondary">
<v-text-field :value="filter.q"
class="input-search background-inherit elevation-0"
solo hide-details clearable overflow single-line
validate-on-blur
autocorrect="off"
autocapitalize="none"
browser-autocomplete="off"
:label="$gettext('Search')"
prepend-inner-icon="search"
color="secondary-dark"
@change="(v) => {updateFilter({'q': v})}"
@keyup.enter.native="(e) => updateQuery({'q': e.target.value})"
@click:clear="() => {updateQuery({'q': ''})}"
></v-text-field>
<v-btn v-if="filter.latlng" icon :title="$gettext('Show more')" class="action-clear-location" @click.stop="clearLocation()">
<v-icon>location_off</v-icon>
</v-btn>
<v-btn icon class="hidden-xs-only action-reload" :title="$gettext('Reload')" @click.stop="refresh()">
<v-icon>refresh</v-icon>
</v-btn>
<v-btn icon class="hidden-xs-only action-reload" :title="$gettext('Reload')" @click.stop="refresh()">
<v-icon>refresh</v-icon>
</v-btn>
<v-btn v-if="settings.view === 'cards'" icon :title="$gettext('Toggle View')" @click.stop="setView('list')">
<v-icon>view_list</v-icon>
</v-btn>
<v-btn v-else-if="settings.view === 'list'" icon :title="$gettext('Toggle View')" @click.stop="setView('mosaic')">
<v-icon>view_comfy</v-icon>
</v-btn>
<v-btn v-else icon :title="$gettext('Toggle View')" @click.stop="setView('cards')">
<v-icon>view_column</v-icon>
</v-btn>
<v-btn v-if="settings.view === 'cards'" icon :title="$gettext('Toggle View')" @click.stop="setView('list')">
<v-icon>view_list</v-icon>
</v-btn>
<v-btn v-else-if="settings.view === 'list'" icon :title="$gettext('Toggle View')"
@click.stop="setView('mosaic')">
<v-icon>view_comfy</v-icon>
</v-btn>
<v-btn v-else icon :title="$gettext('Toggle View')" @click.stop="setView('cards')">
<v-icon>view_column</v-icon>
</v-btn>
<v-btn v-if="!$config.values.readonly && $config.feature('upload')" icon class="hidden-sm-and-down action-upload"
:title="$gettext('Upload')" @click.stop="showUpload()">
<v-icon>cloud_upload</v-icon>
</v-btn>
<v-btn v-if="canDelete && context === 'archive' && config.count.archived > 0" icon
class="hidden-sm-and-down action-delete-all"
:title="$gettext('Delete All')" @click.stop="deleteAll()">
<v-icon>delete_sweep</v-icon>
</v-btn>
<v-btn v-else-if="canUpload" icon class="hidden-sm-and-down action-upload"
:title="$gettext('Upload')" @click.stop="showUpload()">
<v-icon>cloud_upload</v-icon>
</v-btn>
<v-btn icon class="p-expand-search" :title="$gettext('Expand Search')"
@click.stop="searchExpanded = !searchExpanded">
<v-icon>{{ searchExpanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }}</v-icon>
</v-btn>
</template>
<template v-else>
<v-spacer></v-spacer>
<v-btn v-if="canAccessLibrary" icon :title="$gettext('Browse')" class="action-browse" @click.stop="onBrowse">
<v-icon size="20">tab</v-icon>
</v-btn>
<v-btn v-if="onClose !== undefined" icon :title="$gettext('Close')" class="action-close" @click.stop="onClose">
<v-icon>close</v-icon>
</v-btn>
</template>
<v-btn icon class="p-expand-search" :title="$gettext('Expand Search')"
@click.stop="searchExpanded = !searchExpanded">
<v-icon>{{ searchExpanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }}</v-icon>
</v-btn>
</v-toolbar>
<v-card v-show="searchExpanded"
@ -188,77 +166,41 @@
</v-layout>
</v-card-text>
</v-card>
<p-photo-delete-dialog
:show="dialog.delete"
:text="$gettext('Are you sure you want to delete all archived pictures?')"
:action="$gettext('Delete All')"
@cancel="dialog.delete = false" @confirm="batchDelete">
</p-photo-delete-dialog>
</v-form>
</template>
<script>
import Event from "pubsub-js";
import * as options from "options/options";
import Api from "common/api";
import Notify from "common/notify";
export default {
name: 'PPhotoToolbar',
props: {
context: {
type: String,
default: 'photos',
},
filter: {
type: Object,
default: () => {
},
},
staticFilter: {
type: Object,
default: () => {
},
default: () => {},
},
updateFilter: {
type: Function,
default: () => {
},
default: () => {},
},
updateQuery: {
type: Function,
default: () => {
},
default: () => {},
},
settings: {
type: Object,
default: () => {
},
default: () => {},
},
refresh: {
type: Function,
default: () => {
},
},
onClose: {
type: Function,
default: undefined,
},
embedded: {
type: Boolean,
default: false
default: () => {},
},
},
data() {
const features = this.$config.settings().features;
const readonly = this.$config.get("readonly");
return {
experimental: this.$config.get("experimental"),
isFullScreen: !!document.fullscreenElement,
config: this.$config.values,
readonly: readonly,
canUpload: !readonly && !this.embedded && this.$config.allow("files", "upload") && features.upload,
canDelete: !readonly && !this.embedded && this.$config.allow("photos", "delete") && features.delete,
canAccessLibrary: this.$config.allow("photos", "access_library"),
searchExpanded: false,
all: {
countries: [{ID: "", Name: this.$gettext("All Countries")}],
@ -287,9 +229,6 @@ export default {
{value: 'similar', text: this.$gettext('Visual Similarity')},
],
},
dialog: {
delete: false,
},
};
},
computed: {
@ -323,37 +262,7 @@ export default {
},
showUpload() {
Event.publish("dialog.upload");
},
deleteAll() {
if (!this.canDelete) {
return;
}
this.dialog.delete = true;
},
clearLocation() {
this.$router.push({ name: "browse" });
},
onBrowse() {
const route = {name: 'places_browse', query: this.staticFilter};
const routeUrl = this.$router.resolve(route).href;
if (routeUrl) {
window.open(routeUrl, '_blank');
}
},
batchDelete() {
if (!this.canDelete) {
return;
}
this.dialog.delete = false;
Api.post("batch/photos/delete", {"all": true}).then(() => this.onDeleted());
},
onDeleted() {
Notify.success(this.$gettext("Permanently deleted"));
this.$clipboard.clear();
},
}
},
};
</script>

View file

@ -1,102 +0,0 @@
export default class MapStyleControl {
constructor(styles, defaultStyle, setStyle) {
this.styles = styles || MapStyleControl.DEFAULT_STYLES;
this.defaultStyle = defaultStyle || MapStyleControl.DEFAULT_STYLE;
this.setStyle = setStyle;
this.onDocumentClick = this.onDocumentClick.bind(this);
}
getDefaultPosition() {
return "top-right";
}
onAdd(map) {
this.map = map;
this.controlContainer = document.createElement("div");
this.controlContainer.classList.add("maplibregl-ctrl");
this.controlContainer.classList.add("maplibregl-ctrl-group");
this.mapStyleContainer = document.createElement("div");
this.styleButton = document.createElement("button");
this.styleButton.type = "button";
this.mapStyleContainer.classList.add("maplibregl-style-list");
for (const style of this.styles) {
const styleElement = document.createElement("button");
styleElement.type = "button";
styleElement.innerText = style.title;
styleElement.classList.add(style.style, "_");
styleElement.dataset.style = JSON.stringify(style.style);
styleElement.addEventListener("click", (event) => {
const srcElement = event.srcElement;
if (srcElement.classList.contains("active")) {
this.mapStyleContainer.style.display = "none";
this.styleButton.style.display = "block";
return;
}
// Set new map style.
if (typeof this.setStyle === "function") {
this.setStyle(JSON.parse(srcElement.dataset.style));
}
/*
this.mapStyleContainer.style.display = "none";
this.styleButton.style.display = "block";
const elms = this.mapStyleContainer.getElementsByClassName("active");
while (elms[0]) {
elms[0].classList.remove("active");
}
srcElement.classList.add("active");
*/
});
if (style.style === this.defaultStyle) {
styleElement.classList.add("active");
}
this.mapStyleContainer.appendChild(styleElement);
}
this.styleButton.classList.add("maplibregl-ctrl-icon");
this.styleButton.classList.add("maplibregl-style-switcher");
this.styleButton.addEventListener("click", () => {
this.styleButton.style.display = "none";
this.mapStyleContainer.style.display = "block";
});
document.addEventListener("click", this.onDocumentClick);
this.controlContainer.appendChild(this.styleButton);
this.controlContainer.appendChild(this.mapStyleContainer);
return this.controlContainer;
}
onRemove() {
if (
!this.controlContainer ||
!this.controlContainer.parentNode ||
!this.map ||
!this.styleButton
) {
return;
}
this.styleButton.removeEventListener("click", this.onDocumentClick);
this.controlContainer.parentNode.removeChild(this.controlContainer);
document.removeEventListener("click", this.onDocumentClick);
this.map = undefined;
}
onDocumentClick(event) {
if (
this.controlContainer &&
!this.controlContainer.contains(event.target) &&
this.mapStyleContainer &&
this.styleButton
) {
this.mapStyleContainer.style.display = "none";
this.styleButton.style.display = "block";
}
}
}
MapStyleControl.DEFAULT_STYLE = "default";
MapStyleControl.DEFAULT_STYLES = [
{
title: "Default",
style: "default",
},
];

View file

@ -1,14 +1,17 @@
<template>
<div class="video-wrapper" :style="style">
<video :key="source" ref="video" class="video-player" :height="height" :width="width" :autoplay="autoplay"
:style="style" :poster="poster" :loop="loop" preload="auto" controls playsinline
@click.stop @keydown.esc.stop.prevent="$emit('close')">
<source :src="source">
<div class="video-wrapper" :style="style" @click.stop.prevent>
<video :key="source" ref="video" class="video-player"
preload="auto" controls autoplay playsinline @click.stop
@keydown.esc.stop.prevent="$emit('close')">
<source :src="video.url()">
</video>
</div>
</template>
<script>
import Video from "model/video";
import Plyr from 'plyr';
export default {
name: "PVideoPlayer",
props: {
@ -19,13 +22,13 @@ export default {
},
poster: {
type: String,
required: true,
required: false,
default: ""
},
source: {
type: String,
required: true,
default: ""
index: {
type: Number,
required: false,
default: 0
},
width: {
type: Number,
@ -59,37 +62,127 @@ export default {
error: {
type: Function,
default: () => {},
}
},
videos: {
type: Array,
required: false,
default: () => [],
},
album: {
type: Object,
required: false,
default: () => {},
},
},
data() {
const c = this.$config;
return {
refresh: false,
style: `width: 90vw; height: 90vh`,
source: "",
video: new Video(false),
player: false,
current: this.index,
options: {
iconUrl: `${c.staticUri}/video/plyr.svg`,
controls: ['play-large', 'play', 'progress', 'current-time', 'duration', 'mute', 'volume', 'download', 'settings', 'airplay', 'fullscreen'],
settings: ['loop'],
captions: { active: false, language: 'auto', update: false },
hideControls: true,
enabled: true,
autoplay: true,
clickToPlay: true,
disableContextMenu: false,
resetOnEnd: true,
toggleInvert: true,
blankVideo: `${c.staticUri}/video/404.mp4`,
loop: {
active: false,
},
},
};
},
data: () => ({
refresh: false,
style: `width: 90vw; height: 90vh`,
}),
watch: {
source: function (src) {
if (src) {
this.setSrc(src);
}
},
videos: function () {
this.onVideos();
},
},
mounted() {
document.body.classList.add("player");
this.render();
window.addEventListener('keyup', this.onKeyUp);
},
beforeDestroy() {
document.body.classList.remove("player");
this.stop();
},
beforeUnmount() {
try {
if (this.player) {
this.player.destroy();
}
} catch (e) {
console.log(e);
}
window.removeEventListener('keyup', this.onKeyUp);
},
methods: {
videoEl() {
return this.$el.getElementsByTagName('video')[0];
},
updateStyle() {
this.style = `width: ${this.width.toString()}px; height: ${this.height.toString()}px`;
if (!this.video || !this.video.Width) {
return;
}
const size = this.video.playerSize();
this.style = `width: ${size.width.toString()}px; height: ${size.height.toString()}px`;
const plyrEl = this.$el.getElementsByClassName('plyr')[0];
if (plyrEl) {
plyrEl.style.cssText = this.style;
}
this.$el.style.cssText = this.style;
},
currentVideo() {
if(typeof this.videos[this.current] === 'undefined') {
return Video.notFound();
}
return this.videos[this.current];
},
onVideos() {
this.current = this.play;
/*
const video = this.currentVideo();
if (!video || video.Error) {
this.$notify.error(this.$gettext("Not Found"));
return;
}
// Set video parameters.
const size = video.playerSize();
this.loop = video.loop();
this.width = size.width;
this.height = size.height;
this.poster = video.posterUrl();
this.source = video.url(); */
},
render() {
this.updateStyle();
const el = this.videoEl();
if (!el) return;
this.player = new Plyr(el, this.options);
// this.player.on("ended", (ev) => { console.log("event.ended", ev); });
this.play();
},
fullscreen() {
const el = this.videoEl();
@ -97,12 +190,65 @@ export default {
el.requestFullscreen();
},
setSrc(src) {
if (!src) {
play() {
this.video = this.currentVideo();
if (!this.video) {
console.log("render: No current video");
return;
}
this.updateStyle();
this.player.source = {
type: 'video',
title: this.video.Title,
sources: [
{
src: this.video.url(),
// type: this.video.Mime,
},
],
poster: this.video.posterUrl("fit_720"),
};
this.player.loop = this.videos.length === 0 && this.video.loop();
this.player.play();
},
onPrev(ev) {
if(this.videos.length < 2) {
this.current = 0;
return;
} else if(this.current <= 0) {
return;
}
this.player.stop();
this.current--;
this.play();
},
onNext(ev) {
if(this.videos.length < 2) {
this.current = 0;
return;
} else if(this.current >= this.videos.length - 1) {
return;
}
this.player.stop();
this.current++;
this.play();
},
onKeyUp(ev) {
switch(ev.key) {
case "Escape": this.$emit('close'); break;
case "ArrowLeft": this.onPrev(ev); break;
case "ArrowRight": this.onNext(ev); break;
}
},
setSrc(src) {
// console.log("setSrc", src);
if (!src) {
return;
}
const el = this.videoEl();
if (!el) return;
@ -110,6 +256,17 @@ export default {
el.src = src;
el.poster = this.poster;
el.play();
this.updateStyle();
// console.log("el", el);
/* this.player = new Plyr(el);
console.log("this.player", this.player);
this.player.play();
this.player.source = src;
this.player.poster = this.poster;
*/
},
pause() {
const el = this.videoEl();

View file

@ -26,6 +26,8 @@ Additional information can be found in our Developer Guide:
@import url("vendor/icons/material-design-icons.css");
@import url("../../node_modules/vuetify/dist/vuetify.min.css");
@import url("../../node_modules/maplibre-gl/dist/maplibre-gl.css");
@import url("../../node_modules/plyr/dist/plyr.css");
@import url("variables.css");
@import url("typography.css");
@import url("wallpapers.css");
@import url("scrollbar.css");
@ -38,7 +40,7 @@ Additional information can be found in our Developer Guide:
@import url("viewer.css");
@import url("tables.css");
@import url("results.css");
@import url("places.css");
@import url("maps.css");
@import url("labels.css");
@import url("files.css");
@import url("pages.css");
@ -68,7 +70,7 @@ main {
z-index: 1;
}
/* Loading Animations */
/* Page Overlay */
#busy-overlay {
display: none;

View file

@ -1,49 +1,32 @@
/* Layout Breakpoints */
@media (min-width: 1750px) {
/* Extra Large (XL) */
.flex.xlg2 {
flex-basis: 16.666666666666664%;
flex-grow: 0;
max-width: 16.666666666666664%;
}
.flex.xlg10 {
flex-basis: 83.333333333333332%;
flex-grow: 0;
max-width: 83.333333333333332%;
}
}
@media (min-width: 2400px) {
/* Extra Extra Large (XXL) */
.flex.xxl1 {
flex-basis: 8.333333333333332%;
flex-grow: 0;
max-width: 8.333333333333332%;
}
.flex.xxl11 {
flex-basis: 91.66666666666667%;
flex-grow: 0;
max-width: 91.66666666666667%;
}
}
@media (min-width: 2800px) {
/* Ultra Large (UL) */
.flex.ul1 {
@media (min-width: 2550px) {
.flex.xxxl1 {
flex-basis: 8.333333333333332%;
flex-grow: 0;
max-width: 8.333333333333332%;
}
.flex.ul11 {
flex-basis: 91.66666666666667%;
flex-grow: 0;
max-width: 91.66666666666667%;
}
}
/* Layout Widths */
.width-sm, .width-md, .width-lg {
margin-left: auto;
margin-right: auto;
@ -112,13 +95,6 @@
border-radius: 6px;
}
.ra-2,
.ra-2 > a,
.rounded-2,
.rounded-2 > a {
border-radius: 2px !important;
}
.ra-3,
.ra-3 > a,
.rounded-3,
@ -161,13 +137,6 @@
border-radius: 10px !important;
}
.ra-24,
.ra-24 > a,
.rounded-24,
.rounded-24 > a {
border-radius: 24px !important;
}
#photoprism div.v-dialog.v-dialog--fullscreen > div.v-card {
border-radius: 0;
}
@ -181,30 +150,6 @@
border-radius: 5px 5px 0 0;
}
/* Responsive Layout Styles */
@media only screen and (min-width: 960px) {
.pa-1-md-and-up {
padding: 4px!important;
}
.pa-2-md-and-up {
padding: 8px!important;
}
.pa-3-md-and-up {
padding: 16px!important;
}
.pa-3-md-and-up {
padding: 24px!important;
}
.ra-4-table-md-and-up table.v-table {
border-radius: 4px !important;
}
}
/* Page Footer */
footer {

View file

@ -1,4 +1,3 @@
/* Event Logs */
#photoprism .p-logs {
color: white;

26
frontend/src/css/maps.css Normal file
View file

@ -0,0 +1,26 @@
#photoprism .map-control {
position: absolute;
background: transparent;
bottom: 35px;
right: 30px;
z-index: 2;
}
#photoprism.is-rtl .map-control {
right: auto;
left: 30px;
}
#photoprism #map .marker {
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12) !important;
background-color: rgba(0, 0, 0, 0.3);
border-color: #fff;
color: rgba(0, 0, 0, 0.87);
display: block;
border-radius: 50%;
cursor: pointer;
}
#photoprism .maplibregl-ctrl-attrib-inner a {
color: #333 !important;
}

View file

@ -1,5 +1,3 @@
/* App Navigation */
#p-navigation {
z-index: 10;
}
@ -122,12 +120,6 @@ nav .v-list__tile__title.title {
overflow: hidden;
}
#photoprism .p-photo-toolbar.embedded {
position: sticky;
top: 0;
z-index: 1;
}
.p-user-box {
width: 100%;
padding: 0;
@ -315,4 +307,4 @@ nav .v-list__tile__title.title {
#photoprism #mobile-menu .menu-action:hover a i{
fill: #ffffff;
color: #ffffff;
}
}

View file

@ -1,148 +0,0 @@
/* Maps & Places */
#photoprism .map-control {
position: absolute;
background: transparent;
top: 10px;
left: 10px;
z-index: 2;
}
#photoprism.is-rtl .map-control {
left: auto;
right: 10px;
}
#photoprism .map-control .map-control-search {
overflow: hidden;
border-radius: 24px;
}
#photoprism #map .marker {
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12) !important;
background-color: rgba(23, 23, 23, 0.23);
color: rgba(0, 0, 0, 0.87);
display: block;
border-radius: 50%;
cursor: pointer;
border: 1px solid #ffffff99;
}
#photoprism #map .cluster-marker {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1px;
overflow: hidden;
/* background-color: #ffffff99; */
width: 100%;
height: 100%
}
#photoprism #map .counter-bubble {
border-radius: 100%;
position: absolute;
width: 24px;
height: 24px;
font-size: 12px;
display: flex;
color: #ffffff;
top: -5px;
right: -5px;
align-items: center;
justify-content: center;
box-shadow: 0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%);
}
#photoprism #map .cluster-marker > div {
width: 100%;
height: 100%;
background-size: cover;
}
#photoprism .maplibregl-ctrl-attrib-inner a {
color: #333 !important;
}
#photoprism .maplibregl-style-list {
display: none;
}
#photoprism .maplibregl-ctrl-group .maplibregl-style-list button {
background: none;
border: none;
cursor: pointer;
display: block;
font-size: 14px;
padding: 8px 8px 6px;
text-align: right;
width: 100%;
height: auto;
color: #000000;
}
#photoprism .maplibregl-style-list button.active {
font-weight: bold;
}
#photoprism .maplibregl-style-list button:hover {
background-color: rgba(0, 0, 0, 0.05);
}
#photoprism .maplibregl-style-list button + button {
border-top: 1px solid #ddd;
}
#photoprism .maplibregl-style-switcher {
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJDYXBhXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iNTQuODQ5cHgiIGhlaWdodD0iNTQuODQ5cHgiIHZpZXdCb3g9IjAgMCA1NC44NDkgNTQuODQ5IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1NC44NDkgNTQuODQ5OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+PGc+PGc+PHBhdGggZD0iTTU0LjQ5NywzOS42MTRsLTEwLjM2My00LjQ5bC0xNC45MTcsNS45NjhjLTAuNTM3LDAuMjE0LTEuMTY1LDAuMzE5LTEuNzkzLDAuMzE5Yy0wLjYyNywwLTEuMjU0LTAuMTA0LTEuNzktMC4zMThsLTE0LjkyMS01Ljk2OEwwLjM1MSwzOS42MTRjLTAuNDcyLDAuMjAzLTAuNDY3LDAuNTI0LDAuMDEsMC43MTZMMjYuNTYsNTAuODFjMC40NzcsMC4xOTEsMS4yNTEsMC4xOTEsMS43MjksMEw1NC40ODgsNDAuMzNDNTQuOTY0LDQwLjEzOSw1NC45NjksMzkuODE3LDU0LjQ5NywzOS42MTR6Ii8+PHBhdGggZD0iTTU0LjQ5NywyNy41MTJsLTEwLjM2NC00LjQ5MWwtMTQuOTE2LDUuOTY2Yy0wLjUzNiwwLjIxNS0xLjE2NSwwLjMyMS0xLjc5MiwwLjMyMWMtMC42MjgsMC0xLjI1Ni0wLjEwNi0xLjc5My0wLjMyMWwtMTQuOTE4LTUuOTY2TDAuMzUxLDI3LjUxMmMtMC40NzIsMC4yMDMtMC40NjcsMC41MjMsMC4wMSwwLjcxNkwyNi41NiwzOC43MDZjMC40NzcsMC4xOSwxLjI1MSwwLjE5LDEuNzI5LDBsMjYuMTk5LTEwLjQ3OUM1NC45NjQsMjguMDM2LDU0Ljk2OSwyNy43MTYsNTQuNDk3LDI3LjUxMnoiLz48cGF0aCBkPSJNMC4zNjEsMTYuMTI1bDEzLjY2Miw1LjQ2NWwxMi41MzcsNS4wMTVjMC40NzcsMC4xOTEsMS4yNTEsMC4xOTEsMS43MjksMGwxMi41NDEtNS4wMTZsMTMuNjU4LTUuNDYzYzAuNDc3LTAuMTkxLDAuNDgtMC41MTEsMC4wMS0wLjcxNkwyOC4yNzcsNC4wNDhjLTAuNDcxLTAuMjA0LTEuMjM2LTAuMjA0LTEuNzA4LDBMMC4zNTEsMTUuNDFDLTAuMTIxLDE1LjYxNC0wLjExNiwxNS45MzUsMC4zNjEsMTYuMTI1eiIvPjwvZz48L2c+PC9nPjxnPjwvZz48Zz48L2c+PGc+PC9nPjxnPjwvZz48Zz48L2c+PGc+PC9nPjxnPjwvZz48Zz48L2c+PGc+PC9nPjxnPjwvZz48Zz48L2c+PGc+PC9nPjxnPjwvZz48Zz48L2c+PGc+PC9nPjwvc3ZnPg==);
background-position: center;
background-repeat: no-repeat;
background-size: 70%;
}
#photoprism .cluster-control {
background: #2f3031;
position: absolute;
top: auto;
z-index: 2;
bottom: 0;
margin: 0;
padding: 0;
left: 4px;
right: 4px;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
max-height: 90vh;
overflow: hidden;
box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
transition: all 650ms cubic-bezier(0.32, 1, 0.23, 1) 0ms;
}
#photoprism .cluster-control-container {
min-height: 40vh;
}
#photoprism .cluster-control .p-photos {
position: absolute;
top: 45px;
bottom: 0;
left: 0;
right: 0;
overflow: auto;
}
@media only screen and (min-height: 1500px) {
#photoprism .cluster-control-container {
min-height: 45vh;
}
}
@media only screen and (max-width: 599px) {
#photoprism .cluster-control {
left: 0;
right: 0;
}
#photoprism .cluster-control-container {
min-height: 66vh;
}
}

View file

@ -198,128 +198,112 @@ body.chrome #photoprism .search-results .result {
}
#photoprism .list-view {
box-shadow: 0 0 0 0 rgba(0, 0, 0, .2), 0 0 0 0 rgba(0, 0, 0, .14), 0 0 0 0 rgba(0, 0, 0, .12) !important;
box-shadow: 0 0 0 0 rgba(0,0,0,.2),0 0 0 0 rgba(0,0,0,.14),0 0 0 0 rgba(0,0,0,.12) !important;
}
#photoprism .cards-view .result,
#photoprism .mosaic-view .result {
transition: none;
box-shadow: none !important;
}
#photoprism .cards-view .result {
margin: 2px !important;
}
#photoprism .mosaic-view .result {
margin: 0 !important;
#photoprism .mosaic-view .result
{
-webkit-transition-duration: 15ms !important;
-moz-transition-duration: 15ms !important;
-o-transition-duration: 15ms !important;
transition-duration: 15ms !important;
margin: 4px !important;
box-shadow: 0 0 0 0 rgba(0, 0, 0, .2), 0 0 0 0 rgba(0, 0, 0, .14), 0 0 0 0 rgba(0, 0, 0, .12) !important;
border-radius: 6px;
}
#photoprism .search-results .image-container {
aspect-ratio: 1;
contain: layout paint style size;
aspect-ratio: 1;
contain: layout paint style size;
}
#photoprism .cards-view .result .image,
#photoprism .list-view .result .image,
#photoprism .mosaic-view .result.image {
position: relative;
user-select: none;
aspect-ratio: 1;
background-position: center center;
background-size: cover;
background-repeat: no-repeat;
overflow: hidden;
#photoprism .mosaic-view .result.image
{
position: relative;
user-select: none;
aspect-ratio: 1;
background-position: center center;
background-size: cover;
background-repeat: no-repeat;
overflow: hidden;
}
#photoprism .cards-view .result .image button,
#photoprism .list-view .result .image button,
#photoprism .mosaic-view .result.image button {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
width: 28px;
height: 28px;
padding: 0;
transition: background-color .3s cubic-bezier(.25, .8, .5, 1);
position: absolute;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
width: 28px;
height: 28px;
padding: 0;
transition: background-color .3s cubic-bezier(.25,.8,.5,1);
}
#photoprism .cards-view .result .image button {
width: 36px;
height: 36px;
width: 36px;
height: 36px;
}
#photoprism .list-view .result .image button {
width: 44px;
height: 44px;
width: 44px;
height: 44px;
}
#photoprism .cards-view .result .image button:hover,
#photoprism .list-view .result .image button:hover,
#photoprism .mosaic-view .result.image button:hover {
background-color: rgba(255, 255, 255, 0.12);
background-color: rgba(255, 255, 255, 0.12);
}
#photoprism .mosaic-view .result.image i,
#photoprism .list-view .result .image i,
#photoprism .cards-view .result .image i {
color: #e2e2e2;
font-size: 24px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
font-family: Material Icons;
font-style: normal;
font-weight: 400;
text-transform: none;
line-height: 1;
white-space: nowrap;
color: #e2e2e2;
font-size: 24px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
font-family: Material Icons;
font-style: normal;
font-weight: 400;
text-transform: none;
line-height: 1;
white-space: nowrap;
}
#photoprism .cards-view .result .caption {
hyphens: auto;
word-break: break-word;
hyphens: auto;
word-break: break-word;
}
#photoprism .cards-view .result .card-details i {
font-size: 14px;
display: inline-block;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
font-family: Material Icons;
font-style: normal;
font-weight: 400;
text-transform: none;
line-height: 1;
vertical-align: text-bottom;
white-space: nowrap;
font-size: 14px;
display: inline-block;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
font-family: Material Icons;
font-style: normal;
font-weight: 400;
text-transform: none;
line-height: 1;
vertical-align: text-bottom;
white-space: nowrap;
}
#photoprism .cards-view .result.placeholder .card-details i {
width: 14px;
width: 14px;
}
#photoprism .cards-view .result.is-selected {
margin: 7px !important;
}
#photoprism .cards-view .result.is-selected,
#photoprism .mosaic-view .result.is-selected {
margin: 9px !important;
}
#photoprism .list-view .result.is-selected .image {
margin: 2px !important;
}
#photoprism .cards-view .result.is-selected .card-details {
padding-left: 14px !important;
padding-right: 14px !important;
}
#photoprism .cards-view .result.is-selected .card-details .caption {
font-size: 0.85em !important;
text-overflow: ellipsis;
margin: 1px !important;
box-shadow: 0 6px 6px -3px rgba(0, 0, 0, .2), 0 10px 14px 1px rgba(0, 0, 0, .14), 0 4px 18px 3px rgba(0, 0, 0, .12) !important;
}
#photoprism .cards-view .input-select,
@ -531,27 +515,28 @@ body.chrome #photoprism .search-results .result {
/* old browser support */
@supports not (aspect-ratio: 1) {
/* elements with aspect-ratio 1 and without margin */
#photoprism .search-results .image-container,
#photoprism .cards-view .result .image,
#photoprism .list-view .result .image,
#photoprism .list-view .result.image,
#photoprism .mosaic-view .result.is-selected {
height: 0;
padding-bottom: 100%
}
/* elements with aspect-ratio 1 and without margin */
#photoprism .search-results .image-container,
#photoprism .cards-view .result .image,
#photoprism .list-view .result .image,
#photoprism .list-view .result.image,
#photoprism .mosaic-view .result.is-selected {
height: 0;
padding-bottom: 100%
}
/* elements with aspect-ratio 1 and with margin */
#photoprism .mosaic-view .result {
height: 0;
/*
8px is required because this aspect-ratio-fallback doesn't take
margins into consideration
*/
padding-bottom: calc(100% - 8px)
}
/* elements with aspect-ratio 1 and with margin */
#photoprism .mosaic-view .result
{
height: 0;
/*
8px is required because this aspect-ratio-fallback doesn't take
margins into consideration
*/
padding-bottom: calc(100% - 8px)
}
#photoprism .live-player video {
margin-top: 100%;
}
#photoprism .live-player video {
margin-top: 100%;
}
}

View file

@ -1,18 +1,16 @@
@import url("themes/default.css");
@import url("themes/abyss.css");
@import url("themes/carbon.css");
@import url("themes/chrome.css");
@import url("themes/gemstone.css");
@import url("themes/grayscale.css");
@import url("themes/lavender.css");
@import url("themes/mint.css");
@import url("themes/neon.css");
@import url("themes/nordic.css");
@import url("themes/shadow.css");
@import url("themes/vanta.css");
@import url("themes/yellowstone.css");
@import url("themes/abyss.css");
@import url("themes/vanta.css");
@import url("themes/neon.css");
@import url("themes/gemstone.css");
@import url("themes/carbon.css");
@import url("themes/nordic.css");
@import url("themes/lavender.css");
@import url("themes/default.css");
/* Input Style Overrides */
/* General Styles */
:-webkit-autofill,
:-webkit-autofill:hover,
@ -23,15 +21,90 @@ input:-webkit-autofill:focus {
transition: background-color 9999s !important;
}
body.dark-theme #photoprism .v-tabs .v-badge__badge {
color: #000000;
}
body.dark-theme #photoprism .theme--light.v-table thead th {
color: #ffffff;
}
.card {
background-color: #e0e0e0;
}
body.dark-theme .card {
background-color: #1a1a1a;
}
body.dark-theme #photoprism .v-table .theme--light.v-text-field--solo>.v-input__control>.v-input__slot {
background: transparent;
}
body.dark-theme .theme--light.v-list .v-list__tile--highlighted,
body.dark-theme .theme--light.v-list a:hover {
background: rgba(255,255,255,0.3) !important;
}
body.dark-theme .v-input input::placeholder {
color: rgba(255,255,255,0.5) !important;
}
.v-text-field>.v-input__control>.v-input__slot:after,
.v-text-field>.v-input__control>.v-input__slot:before {
display: none !important;
}
/*
body.dark-theme .v-text-field.v-text-field--enclosed .v-text-field__details {
padding-left: 0;
}*/
.theme--light.v-list .v-list__tile--highlighted,
.theme--light.v-list a:hover {
background: rgba(155,155,155,0.3) !important;
}
body.dark-theme #photoprism .map-control .theme--light.v-input:not(.v-input--is-disabled) i,
body.dark-theme #photoprism .map-control .theme--light.v-input:not(.v-input--is-disabled) input {
color: #333333;
}
body:not(.dark-theme) .theme--dark.v-btn.v-btn--disabled:not(.v-btn--icon):not(.v-btn--flat):not(.v-btn--outline) {
color: rgb(51, 51, 51)!important;
background-color: #d9dadc!important;
}
body.dark-theme #photoprism .theme--light.v-chip,
body.dark-theme #photoprism .v-card .theme--light.v-text-field--box>.v-input__control>.v-input__slot {
background: #00000033;
}
body.dark-theme #photoprism,
body.dark-theme #photoprism .p-page a,
body.dark-theme #photoprism .v-datatable a,
body.dark-theme #photoprism .theme--light.v-expansion-panel .v-expansion-panel__container,
body.dark-theme #photoprism .theme--light.v-tabs__bar .v-tabs__div,
body.dark-theme #photoprism .theme--light {
color: #ffffff;
caret-color: #ffffff;
stroke: #ffffff;
}
body.dark-theme #photoprism .theme--light.v-label {
color: #ffffffaa;
}
body.dark-theme #photoprism .theme--light.v-select .v-select__selections {
color: #ffffffee;
}
#photoprism .v-select.v-text-field--box .v-select__selections {
padding-top: 20px;
}
/* Exceptions */
#photoprism .theme--light.v-text-field--solo.background-inherit>.v-input__control>.v-input__slot {
background: inherit;
box-shadow: none;

View file

@ -33,8 +33,8 @@ body.dark-theme.theme-abyss,
background: #333;
}
#photoprism.theme-abyss .theme--light.v-text-field--solo>.v-input__control>.v-input__slot {
background: rgba(250, 250, 255, 0.1);
#photoprism.theme-abyss .theme--light.v-text-field--solo > .v-input__control > .v-input__slot {
background: #262626;
}
#photoprism.theme-abyss .theme--light.v-input--selection-controls.v-input--is-disabled .v-icon {

View file

@ -1,4 +1,4 @@
/* Carbon Dark Theme */
/* Carbon Theme */
body.dark-theme.theme-carbon,
.theme-carbon .v-content__wrap,

View file

@ -1,146 +0,0 @@
/* Chrome Dark Theme */
body.dark-theme.theme-chrome,
.theme-chrome .v-content__wrap,
.theme-chrome .p-page,
.theme-chrome .form,
.theme-chrome .v-content {
background: #1d1d1d !important;
}
#photoprism.theme-chrome .theme--light.v-small-dialog__content,
#photoprism.theme-chrome .theme--light.v-sheet,
#photoprism.theme-chrome .theme--light.v-card,
.theme-chrome .application.theme--light {
background: #202020;
}
#photoprism.container.theme-chrome {
background-image: linear-gradient(160deg, #808080 0%, #262626 100%);
}
#photoprism.theme-chrome .theme--light .v-table {
background: #262626;
}
body.dark-theme #photoprism.theme-chrome,
body.dark-theme #photoprism.theme-chrome .v-datatable a,
body.dark-theme #photoprism.theme-chrome .theme--light.v-expansion-panel .v-expansion-panel__container,
body.dark-theme #photoprism.theme-chrome .theme--light.v-tabs__bar .v-tabs__div {
color: #ffffff;
caret-color: #ffffff;
stroke: #ffffff;
}
body.dark-theme #photoprism.theme-chrome .p-page a,
body.dark-theme #photoprism.theme-chrome .theme--light,
body.dark-theme #photoprism.theme-chrome #p-navigation .navigation-menu.theme--dark.v-list,
body.dark-theme #photoprism.theme-chrome #p-navigation .navigation-menu .theme--dark.v-icon {
color: #d8d8d8;
caret-color: #d8d8d8;
stroke: #d8d8d8;
}
body.dark-theme #photoprism.theme-chrome .v-input--selection-controls__input .theme--light {
color: #8e8e8e;
caret-color: #8e8e8e;
stroke: #8e8e8e;
}
body.dark-theme #photoprism.theme-chrome #p-navigation .navigation-menu .theme--dark.v-icon {
color: inherit;
}
#photoprism.theme-chrome .theme--light.v-table thead th,
#photoprism.theme-chrome .theme--light.v-table tbody td {
color: #fff;
}
#photoprism.theme-chrome .theme--light.v-table tbody tr:hover {
background: #2b2b2b;
}
#photoprism.theme-chrome .theme--light.v-text-field--solo>.v-input__control>.v-input__slot,
#photoprism.theme-chrome .theme--light.v-text-field--solo>.v-input__control>.v-input__slot {
background: rgba(255, 255, 255, 0.1);
}
#photoprism.theme-chrome .secondary--text {
color: rgba(255, 255, 255, 0.2)!important;
}
body.dark-theme #photoprism.theme-chrome #map .secondary-dark.theme--light,
body.dark-theme #photoprism.theme-chrome #map .secondary-dark--text {
color: #000000!important;
caret-color: #000000!important;
stroke: #000000!important;
}
body.dark-theme #photoprism.theme-chrome .map-control .theme--light {
color: #757575;
caret-color: #757575;
stroke: #757575;
}
body.dark-theme #photoprism.theme-chrome .v-card .theme--light.v-text-field--box>.v-input__control>.v-input__slot {
background: rgba(255, 255, 255, 0.04);
}
#photoprism.theme-chrome .theme--light.v-input--selection-controls.v-input--is-disabled .v-icon {
color: #999 !important;
}
#photoprism.theme-chrome .theme--light.v-input--selection-controls.v-input--is-disabled .v-icon,
#photoprism.theme-chrome .theme--light.v-input--is-disabled .v-label,
#photoprism.theme-chrome .theme--light.v-input--is-disabled input,
#photoprism.theme-chrome .theme--light.v-input--is-disabled textarea {
color: #aaaaaa;
}
#photoprism.theme-chrome .theme--light.v-list {
background: #232425;
}
#photoprism.theme-chrome a.text-link {
color: #ffffff !important;
}
#photoprism.theme-chrome .footer .body-link {
color: #ffffff !important;
}
#photoprism.theme-chrome .theme--light.v-list .v-list__tile__sub-title,
#photoprism.theme-chrome .accent--text {
color: #ffffff !important;
}
#photoprism.theme-chrome .theme--light.v-input:not(.v-input--is-disabled) input,
#photoprism.theme-chrome .theme--light.v-input:not(.v-input--is-disabled) textarea {
color: #fff;
}
#photoprism.theme-chrome .theme--light.v-btn.v-btn--disabled,
#photoprism.theme-chrome .theme--light.v-btn.v-btn--disabled .v-btn__loading,
#photoprism.theme-chrome .theme--light.v-btn.v-btn--disabled .v-icon {
color: #999 !important;
}
#photoprism.theme-chrome .theme--light.v-list .v-list__tile__mask {
color: #cccccc;
background: transparent;
}
#photoprism.theme-chrome .cards-view .card.is-selected .card-details {
color: #fff;
background-color: #5C5C5C;
}
#photoprism.theme-chrome .cards-view .card.is-selected,
#photoprism.theme-chrome .cards-view .card.is-selected .card-background {
background-color: #5C5C5C !important;
}
#photoprism.theme-chrome .cards-view .card.is-selected .card-details .v-icon {
color: #fff;
}

View file

@ -1,75 +1,7 @@
/* Component Color Defaults */
/* Default Theme */
.card {
background-color: #e0e0e0;
}
body.dark-theme .card {
background-color: #1a1a1a;
}
body.dark-theme #photoprism .v-table .theme--light.v-text-field--solo>.v-input__control>.v-input__slot {
background: transparent;
}
body.dark-theme .theme--light.v-list .v-list__tile--highlighted,
body.dark-theme .theme--light.v-list a:hover {
background: rgba(255,255,255,0.3) !important;
}
body.dark-theme .v-input input::placeholder {
color: rgba(255,255,255,0.5) !important;
}
body.dark-theme .theme--light .v-table {
background: #303030;
}
body.dark-theme .theme--light.v-table tbody tr:hover {
background: #343434;
}
.theme--light.v-list .v-list__tile--highlighted,
.theme--light.v-list a:hover {
background: rgba(155,155,155,0.3) !important;
}
body:not(.dark-theme) .theme--dark.v-btn.v-btn--disabled:not(.v-btn--icon):not(.v-btn--flat):not(.v-btn--outline) {
color: rgb(51, 51, 51)!important;
background-color: #d9dadc!important;
}
body.dark-theme #photoprism .theme--light.v-chip,
body.dark-theme #photoprism .v-card .theme--light.v-text-field--box>.v-input__control>.v-input__slot {
background: #00000033;
}
body.dark-theme #photoprism,
body.dark-theme #photoprism .p-page a,
body.dark-theme #photoprism .v-datatable a,
body.dark-theme #photoprism .theme--light.v-expansion-panel .v-expansion-panel__container,
body.dark-theme #photoprism .theme--light.v-tabs__bar .v-tabs__div,
body.dark-theme #photoprism .theme--light {
color: #ffffff;
caret-color: #ffffff;
stroke: #ffffff;
}
body.dark-theme #photoprism .theme--light.v-label {
color: #ffffffaa;
}
body.dark-theme #photoprism .theme--light.v-select .v-select__selections {
color: #ffffffee;
}
body.dark-theme .theme--light.v-small-dialog__content,
body.dark-theme .theme--light.v-sheet,
body.dark-theme .theme--light.v-card {
background: #2f3031;
}
body.dark-theme .theme--light.v-text-field--solo>.v-input__control>.v-input__slot {
#photoprism.theme-default .theme--light.v-text-field--solo>.v-input__control>.v-input__slot,
#photoprism.theme-default .theme--light.v-text-field--solo>.v-input__control>.v-input__slot {
background: rgba(250, 250, 255, 0.1);
}
@ -88,10 +20,7 @@ body.dark-theme #photoprism .p-page a,
body.dark-theme #photoprism .v-datatable a,
body.dark-theme #photoprism .theme--light.v-expansion-panel .v-expansion-panel__container,
body.dark-theme #photoprism .theme--light.v-tabs__bar .v-tabs__div,
body.dark-theme #photoprism .theme--light,
body.dark-theme #photoprism .theme--light.v-table thead th,
body.dark-theme .theme--light.v-table thead th,
body.dark-theme .theme--light.v-table tbody td {
body.dark-theme #photoprism .theme--light {
color: #ffffff;
}
@ -107,12 +36,6 @@ body.dark-theme #photoprism .theme--light.v-select .v-select__selections {
padding-top: 20px;
}
body.dark-theme #photoprism .v-tabs .v-badge__badge {
color: #000000;
}
/* Default Theme */
body.dark-theme.theme-default,
.theme-default .v-content__wrap,
.theme-default .p-page,
@ -128,6 +51,10 @@ body.dark-theme.theme-default,
background: #2f3031;
}
#photoprism.theme-default .theme--light .v-table {
background: #37393a;
}
#photoprism.container.theme-default {
background-image: linear-gradient(160deg, #808080 0%, #262626 100%);
}
@ -141,10 +68,6 @@ body.dark-theme.theme-default,
stroke: rgba(196, 241, 229, 0.3);
}
#photoprism.theme-default .theme--light .v-table {
background: #37393a;
}
#photoprism.theme-default .theme--light.v-table thead th,
#photoprism.theme-default .theme--light.v-table tbody td {
color: #fff;
@ -158,11 +81,6 @@ body.dark-theme.theme-default,
color: #999 !important;
}
#photoprism.theme-default .theme--light.v-text-field--solo>.v-input__control>.v-input__slot,
#photoprism.theme-default .theme--light.v-text-field--solo>.v-input__control>.v-input__slot {
background: rgba(250, 250, 255, 0.1);
}
#photoprism.theme-default .theme--light.v-input--selection-controls.v-input--is-disabled .v-icon,
#photoprism.theme-default .theme--light.v-input--is-disabled .v-label,
#photoprism.theme-default .theme--light.v-input--is-disabled input,
@ -205,12 +123,12 @@ body.dark-theme.theme-default,
#photoprism.theme-default .cards-view .card.is-selected .card-details {
color: #fff;
background-color: #636869;
background-color: #4b4e4f;
}
#photoprism.theme-default .cards-view .card.is-selected,
#photoprism.theme-default .cards-view .card.is-selected .card-background {
background-color: #636869 !important;
background-color: #4b4e4f !important;
}
#photoprism.theme-default .cards-view .card.is-selected .card-details .v-icon {

View file

@ -1,4 +1,4 @@
/* Gemstone Dark Theme */
/* Gemstone Theme */
body.dark-theme.theme-gemstone,
.theme-gemstone .v-content__wrap,

View file

@ -1,4 +1,4 @@
/* Grayscale Dark Theme */
/* Grayscale Theme */
body.dark-theme.theme-grayscale {
background: #525252 !important;

View file

@ -1,92 +0,0 @@
/* Mint Dark Theme */
body.dark-theme.theme-mint,
.theme-mint .v-content__wrap,
.theme-mint .p-page,
.theme-mint .form,
.theme-mint .v-content {
background: #121212 !important;
}
#photoprism.theme-mint .theme--light.v-small-dialog__content,
#photoprism.theme-mint .theme--light.v-sheet,
#photoprism.theme-mint .theme--light.v-card,
.theme-mint .application.theme--light {
background: #1e1e1e;
}
#photoprism.container.theme-mint {
background-image: linear-gradient(160deg, #808080 0%, #262626 100%);
}
#photoprism.theme-mint .theme--light .v-table {
background: #1a1a1a;
}
#photoprism.theme-mint .theme--light.v-table thead th,
#photoprism.theme-mint .theme--light.v-table tbody td {
color: #fff;
}
#photoprism.theme-mint .theme--light.v-table tbody tr:hover {
background: #2b2b2b;
}
#photoprism.theme-mint .theme--light.v-input--selection-controls.v-input--is-disabled .v-icon {
color: #999 !important;
}
#photoprism.theme-mint .theme--light.v-input--selection-controls.v-input--is-disabled .v-icon,
#photoprism.theme-mint .theme--light.v-input--is-disabled .v-label,
#photoprism.theme-mint .theme--light.v-input--is-disabled input,
#photoprism.theme-mint .theme--light.v-input--is-disabled textarea {
color: #ccc;
}
#photoprism.theme-mint .theme--light.v-list {
background: #232425;
}
#photoprism.theme-mint a.text-link {
color: #f6f7e8 !important;
}
#photoprism.theme-mint .footer .body-link {
color: #f6f7e8 !important;
}
#photoprism.theme-mint .theme--light.v-list .v-list__tile__sub-title,
#photoprism.theme-mint .accent--text {
color: #2bb14c !important;
}
#photoprism.theme-mint .theme--light.v-input:not(.v-input--is-disabled) input,
#photoprism.theme-mint .theme--light.v-input:not(.v-input--is-disabled) textarea {
color: #fff;
}
#photoprism.theme-mint .theme--light.v-btn.v-btn--disabled,
#photoprism.theme-mint .theme--light.v-btn.v-btn--disabled .v-btn__loading,
#photoprism.theme-mint .theme--light.v-btn.v-btn--disabled .v-icon {
color: #999 !important;
}
#photoprism.theme-mint .theme--light.v-list .v-list__tile__mask {
color: #cccccc;
background: transparent;
}
#photoprism.theme-mint .cards-view .card.is-selected .card-details {
color: #fff;
background-color: #333333;
}
#photoprism.theme-mint .cards-view .card.is-selected,
#photoprism.theme-mint .cards-view .card.is-selected .card-background {
background-color: #333333 !important;
}
#photoprism.theme-mint .cards-view .card.is-selected .card-details .v-icon {
color: #fff;
}

View file

@ -1,4 +1,4 @@
/* Neon Dark Theme */
/* Neon Theme */
.theme-neon .v-snack.v-snack--active.v-snack--bottom,
.theme-neon .v-speed-dial .v-btn--floating.v-btn--small {

View file

@ -1,4 +1,4 @@
/* Shadow Dark Theme */
/* Shadow Theme */
body.dark-theme.theme-shadow {
background: #444 !important;

View file

@ -1,4 +1,4 @@
/* Vanta Dark Theme */
/* Vanta Theme */
body.dark-theme.theme-vanta {
background: #212121 !important;

View file

@ -1,4 +1,4 @@
/* Yellowstone Dark Theme */
/* Yellowstone Theme */
body.dark-theme.theme-yellowstone,
.theme-yellowstone .v-content__wrap,

View file

@ -39,10 +39,6 @@ button.v-btn.compact {
height: 34px;
}
.caption.filename {
font-weight: 500;
}
/* Line Height */
.lh-15 {

View file

@ -0,0 +1,4 @@
:root {
--plyr-color-main: #2f303164; /* #d3cbfc66; */
--plyr-video-control-background-hover: #2f303190;
}

View file

@ -10,7 +10,7 @@
top: 0;
bottom: 0;
right: 0;
background-color: rgba(0,0,0,0.72);
background-color: rgba(0,0,0,0.74);
}
#photoprism .video-wrapper {

View file

@ -1,5 +1,5 @@
<template>
<v-dialog :value="show" lazy persistent max-width="360" class="p-photo-delete-dialog" @keydown.esc="cancel">
<v-dialog :value="show" lazy persistent max-width="350" class="p-photo-delete-dialog" @keydown.esc="cancel">
<v-card raised elevation="24">
<v-container fluid class="pb-2 pr-2 pl-2">
<v-layout row wrap>
@ -7,25 +7,18 @@
<v-icon size="54" color="secondary-dark lighten-1">delete_outline</v-icon>
</v-flex>
<v-flex xs9 text-xs-left align-self-center>
<div v-if="text === ''" class="subheading pr-1">
<div class="subheading pr-1">
<translate>Are you sure you want to permanently delete these pictures?</translate>
</div>
<div v-else class="subheading pr-1">
{{ text }}
</div>
</v-flex>
<v-flex xs12 text-xs-right class="pt-3">
<v-btn depressed color="secondary-light" class="action-cancel" @click.stop="cancel">
<translate key="Cancel">Cancel</translate>
</v-btn>
<v-btn v-if="action === ''" color="primary-button" depressed dark class="action-confirm"
<v-btn color="primary-button" depressed dark class="action-confirm"
@click.stop="confirm">
<translate key="Delete">Delete</translate>
</v-btn>
<v-btn v-else color="primary-button" depressed dark class="action-confirm"
@click.stop="confirm">
{{ action }}
</v-btn>
</v-flex>
</v-layout>
</v-container>
@ -37,14 +30,6 @@ export default {
name: 'PPhotoDeleteDialog',
props: {
show: Boolean,
text: {
type: String,
default: "",
},
action: {
type: String,
default: "",
},
},
data() {
return {};

View file

@ -24,11 +24,12 @@
</v-toolbar>
<v-tabs
v-model="active"
flat grow
class="form"
flat
grow
color="secondary"
slider-color="secondary-dark"
:height="$vuetify.breakpoint.smAndDown ? 48 : 64"
class="form"
>
<v-tab id="tab-details" ripple>
<v-icon v-if="$vuetify.breakpoint.smAndDown" :title="$gettext('Details')">edit</v-icon>
@ -81,7 +82,7 @@
</v-tab>
<v-tabs-items touchless>
<v-tab-item lazy>
<v-tab-item>
<p-tab-photo-details :key="uid" ref="details" :model="model" :uid="uid"
@close="close" @prev="prev" @next="next"></p-tab-photo-details>
</v-tab-item>

View file

@ -1,408 +1,412 @@
<template>
<div class="p-tab p-tab-photo-details">
<v-form ref="form" lazy-validation
dense class="p-form-photo-details-meta" accept-charset="UTF-8"
@submit.prevent="save">
<v-layout class="pa-2" row wrap align-top fill-height>
<v-flex class="pa-2 p-photo" xs12 sm4 md2 fill-height>
<v-card tile
class="pa-0 ma-0 elevation-0"
:title="model.Title">
<v-img v-touch="{left, right}"
:src="model.thumbnailUrl('tile_500')"
aspect-ratio="1"
class="card darken-1 elevation-0 clickable"
@click.exact="openPhoto()"
>
</v-img>
<v-container fluid>
<v-form ref="form" lazy-validation
dense class="p-form-photo-details-meta" accept-charset="UTF-8"
@submit.prevent="save">
<v-layout row wrap align-top fill-height>
<v-flex
class="p-photo pa-2"
xs12 sm4 md2
>
<v-card tile
class="ma-0 elevation-0"
:title="model.Title">
<v-img v-touch="{left, right}"
:src="model.thumbnailUrl('tile_500')"
aspect-ratio="1"
class="card darken-1 elevation-0 clickable"
@click.exact="openPhoto()"
>
</v-img>
</v-card>
</v-flex>
<v-flex xs12 sm8 md10 fill-height>
<v-layout row wrap>
<v-flex xs12 lg6 class="pa-2">
<v-text-field
v-model="model.Title"
:append-icon="model.TitleSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:rules="[textRule]"
hide-details box flat
:label="$pgettext('Photo', 'Title')"
placeholder=""
color="secondary-dark"
browser-autocomplete="off"
class="input-title"
></v-text-field>
</v-flex>
</v-card>
</v-flex>
<v-flex xs12 sm8 md10 fill-height>
<v-layout row wrap>
<v-flex xs12 lg6 class="pa-2">
<v-text-field
v-model="model.Title"
:append-icon="model.TitleSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:rules="[textRule]"
hide-details box flat
:label="$pgettext('Photo', 'Title')"
placeholder=""
color="secondary-dark"
browser-autocomplete="off"
class="input-title"
></v-text-field>
</v-flex>
<v-flex xs4 md1 pa-2>
<v-autocomplete
v-model="model.Day"
:append-icon="model.TakenSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:error="invalidDate"
:label="$gettext('Day')"
browser-autocomplete="off"
hide-details box flat hide-no-data
color="secondary-dark"
:items="options.Days()"
class="input-day"
@change="updateTime">
</v-autocomplete>
</v-flex>
<v-flex xs4 md1 pa-2>
<v-autocomplete
v-model="model.Month"
:append-icon="model.TakenSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:error="invalidDate"
:label="$gettext('Month')"
browser-autocomplete="off"
hide-details box flat hide-no-data
color="secondary-dark"
:items="options.MonthsShort()"
class="input-month"
@change="updateTime">
</v-autocomplete>
</v-flex>
<v-flex xs4 md2 pa-2>
<v-autocomplete
v-model="model.Year"
:append-icon="model.TakenSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:error="invalidDate"
:label="$gettext('Year')"
browser-autocomplete="off"
hide-details box flat hide-no-data
color="secondary-dark"
:items="options.Years()"
class="input-year"
@change="updateTime">
</v-autocomplete>
</v-flex>
<v-flex xs4 md1 pa-2>
<v-autocomplete
v-model="model.Day"
:append-icon="model.TakenSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:error="invalidDate"
:label="$gettext('Day')"
browser-autocomplete="off"
hide-details box flat hide-no-data
color="secondary-dark"
:items="options.Days()"
class="input-day"
@change="updateTime">
</v-autocomplete>
</v-flex>
<v-flex xs4 md1 pa-2>
<v-autocomplete
v-model="model.Month"
:append-icon="model.TakenSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:error="invalidDate"
:label="$gettext('Month')"
browser-autocomplete="off"
hide-details box flat hide-no-data
color="secondary-dark"
:items="options.MonthsShort()"
class="input-month"
@change="updateTime">
</v-autocomplete>
</v-flex>
<v-flex xs4 md2 pa-2>
<v-autocomplete
v-model="model.Year"
:append-icon="model.TakenSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:error="invalidDate"
:label="$gettext('Year')"
browser-autocomplete="off"
hide-details box flat hide-no-data
color="secondary-dark"
:items="options.Years()"
class="input-year"
@change="updateTime">
</v-autocomplete>
</v-flex>
<v-flex xs6 md2 class="pa-2">
<v-text-field
v-model="time"
:append-icon="model.TakenSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:label="model.timeIsUTC() ? $gettext('Time UTC') : $gettext('Local Time')"
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
hide-details box flat
return-masked-value mask="##:##:##"
color="secondary-dark"
class="input-local-time"
></v-text-field>
</v-flex>
<v-flex xs6 md2 class="pa-2">
<v-text-field
v-model="time"
:append-icon="model.TakenSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:label="model.timeIsUTC() ? $gettext('Time UTC') : $gettext('Local Time')"
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
hide-details box flat
return-masked-value mask="##:##:##"
color="secondary-dark"
class="input-local-time"
></v-text-field>
</v-flex>
<v-flex xs6 sm6 md6 lg3 class="pa-2">
<v-autocomplete
v-model="model.TimeZone"
:disabled="disabled"
:label="$gettext('Time Zone')"
browser-autocomplete="off"
hide-details box flat hide-no-data
color="secondary-dark"
item-value="ID"
item-text="Name"
:items="options.TimeZones()"
class="input-timezone"
@change="updateTime">
</v-autocomplete>
</v-flex>
<v-flex xs6 sm6 md6 lg3 class="pa-2">
<v-autocomplete
v-model="model.TimeZone"
:disabled="disabled"
:label="$gettext('Time Zone')"
browser-autocomplete="off"
hide-details box flat hide-no-data
color="secondary-dark"
item-value="ID"
item-text="Name"
:items="options.TimeZones()"
class="input-timezone"
@change="updateTime">
</v-autocomplete>
</v-flex>
<v-flex xs12 sm8 md4 lg3 class="pa-2">
<v-autocomplete
v-model="model.Country"
:append-icon="model.PlaceSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:readonly="!!(model.Lat || model.Lng)"
:label="$gettext('Country')" hide-details box flat
hide-no-data
browser-autocomplete="off"
color="secondary-dark"
item-value="Code"
item-text="Name"
:items="countries"
class="input-country">
</v-autocomplete>
</v-flex>
<v-flex xs12 sm8 md4 lg3 class="pa-2">
<v-autocomplete
v-model="model.Country"
:append-icon="model.PlaceSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:readonly="!!(model.Lat || model.Lng)"
:label="$gettext('Country')" hide-details box flat
hide-no-data
browser-autocomplete="off"
color="secondary-dark"
item-value="Code"
item-text="Name"
:items="countries"
class="input-country">
</v-autocomplete>
</v-flex>
<v-flex xs4 md2 lg2 class="pa-2">
<v-text-field
v-model="model.Altitude"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
:label="$gettext('Altitude (m)')"
placeholder=""
color="secondary-dark"
class="input-altitude"
></v-text-field>
</v-flex>
<v-flex xs4 md2 lg2 class="pa-2">
<v-text-field
v-model="model.Altitude"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
:label="$gettext('Altitude (m)')"
placeholder=""
color="secondary-dark"
class="input-altitude"
></v-text-field>
</v-flex>
<v-flex xs4 sm6 md3 lg2 class="pa-2">
<v-text-field
v-model="model.Lat"
:append-icon="model.PlaceSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
:label="$gettext('Latitude')"
placeholder=""
color="secondary-dark"
class="input-latitude"
@paste="pastePosition"
></v-text-field>
</v-flex>
<v-flex xs4 sm6 md3 lg2 class="pa-2">
<v-text-field
v-model="model.Lat"
:append-icon="model.PlaceSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
:label="$gettext('Latitude')"
placeholder=""
color="secondary-dark"
class="input-latitude"
></v-text-field>
</v-flex>
<v-flex xs4 sm6 md3 lg2 class="pa-2">
<v-text-field
v-model="model.Lng"
:append-icon="model.PlaceSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
:label="$gettext('Longitude')"
placeholder=""
color="secondary-dark"
class="input-longitude"
@paste="pastePosition"
></v-text-field>
</v-flex>
<v-flex xs4 sm6 md3 lg2 class="pa-2">
<v-text-field
v-model="model.Lng"
:append-icon="model.PlaceSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
:label="$gettext('Longitude')"
placeholder=""
color="secondary-dark"
class="input-longitude"
></v-text-field>
</v-flex>
<v-flex xs12 md6 pa-2 class="p-camera-select">
<v-select
v-model="model.CameraID"
:append-icon="model.CameraSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:label="$gettext('Camera')"
:menu-props="{'maxHeight':346}"
browser-autocomplete="off"
hide-details box flat
color="secondary-dark"
item-value="ID"
item-text="Name"
:items="cameraOptions"
class="input-camera">
</v-select>
</v-flex>
<v-flex xs12 md6 pa-2 class="p-camera-select">
<v-select
v-model="model.CameraID"
:append-icon="model.CameraSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:label="$gettext('Camera')"
:menu-props="{'maxHeight':346}"
browser-autocomplete="off"
hide-details box flat
color="secondary-dark"
item-value="ID"
item-text="Name"
:items="cameraOptions"
class="input-camera">
</v-select>
</v-flex>
<v-flex xs6 md3 class="pa-2">
<v-text-field
v-model="model.Iso"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
label="ISO"
placeholder=""
color="secondary-dark"
class="input-iso"
></v-text-field>
</v-flex>
<v-flex xs6 md3 class="pa-2">
<v-text-field
v-model="model.Iso"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
label="ISO"
placeholder=""
color="secondary-dark"
class="input-iso"
></v-text-field>
</v-flex>
<v-flex xs6 md3 class="pa-2">
<v-text-field
v-model="model.Exposure"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
:label="$gettext('Exposure')"
placeholder=""
color="secondary-dark"
class="input-exposure"
></v-text-field>
</v-flex>
<v-flex xs6 md3 class="pa-2">
<v-text-field
v-model="model.Exposure"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
:label="$gettext('Exposure')"
placeholder=""
color="secondary-dark"
class="input-exposure"
></v-text-field>
</v-flex>
<v-flex xs12 md6 pa-2 class="p-lens-select">
<v-select
v-model="model.LensID"
:append-icon="model.CameraSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:label="$gettext('Lens')"
:menu-props="{'maxHeight':346}"
browser-autocomplete="off"
hide-details box flat
color="secondary-dark"
item-value="ID"
item-text="Name"
:items="lensOptions"
class="input-lens">
</v-select>
</v-flex>
<v-flex xs12 md6 pa-2 class="p-lens-select">
<v-select
v-model="model.LensID"
:append-icon="model.CameraSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:label="$gettext('Lens')"
:menu-props="{'maxHeight':346}"
browser-autocomplete="off"
hide-details box flat
color="secondary-dark"
item-value="ID"
item-text="Name"
:items="lensOptions"
class="input-lens">
</v-select>
</v-flex>
<v-flex xs6 md3 class="pa-2">
<v-text-field
v-model="model.FNumber" f
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
:label="$gettext('F Number')"
placeholder=""
color="secondary-dark"
class="input-fnumber"
></v-text-field>
</v-flex>
<v-flex xs6 md3 class="pa-2">
<v-text-field
v-model="model.FNumber" f
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
autocorrect="off"
autocapitalize="none"
:label="$gettext('F Number')"
placeholder=""
color="secondary-dark"
class="input-fnumber"
></v-text-field>
</v-flex>
<v-flex xs6 md3 class="pa-2">
<v-text-field
v-model="model.FocalLength"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
:label="$gettext('Focal Length')"
placeholder=""
color="secondary-dark"
class="input-focal-length"
></v-text-field>
</v-flex>
<v-flex xs6 md3 class="pa-2">
<v-text-field
v-model="model.FocalLength"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
:label="$gettext('Focal Length')"
placeholder=""
color="secondary-dark"
class="input-focal-length"
></v-text-field>
</v-flex>
<v-flex xs12 md6 class="pa-2">
<v-text-field
v-model="model.Details.Artist"
:append-icon="model.Details.ArtistSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:rules="[textRule]"
hide-details box flat
browser-autocomplete="off"
:label="$gettext('Artist')"
placeholder=""
color="secondary-dark"
class="input-artist"
></v-text-field>
</v-flex>
<v-flex xs12 md6 class="pa-2">
<v-text-field
v-model="model.Details.Artist"
:append-icon="model.Details.ArtistSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:rules="[textRule]"
hide-details box flat
browser-autocomplete="off"
:label="$gettext('Artist')"
placeholder=""
color="secondary-dark"
class="input-artist"
></v-text-field>
</v-flex>
<v-flex xs6 md3 class="pa-2">
<v-text-field
v-model="model.Details.Copyright"
:append-icon="model.Details.CopyrightSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:rules="[textRule]"
hide-details box flat
browser-autocomplete="off"
:label="$gettext('Copyright')"
placeholder=""
color="secondary-dark"
class="input-copyright"
></v-text-field>
</v-flex>
<v-flex xs6 md3 class="pa-2">
<v-text-field
v-model="model.Details.Copyright"
:append-icon="model.Details.CopyrightSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:rules="[textRule]"
hide-details box flat
browser-autocomplete="off"
:label="$gettext('Copyright')"
placeholder=""
color="secondary-dark"
class="input-copyright"
></v-text-field>
</v-flex>
<v-flex xs6 md3 class="pa-2">
<v-textarea
v-model="model.Details.License"
:append-icon="model.Details.LicenseSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:rules="[textRule]"
hide-details box flat
browser-autocomplete="off"
auto-grow
:label="$gettext('License')"
placeholder=""
:rows="1"
color="secondary-dark"
class="input-license"
></v-textarea>
</v-flex>
<v-flex xs6 md3 class="pa-2">
<v-textarea
v-model="model.Details.License"
:append-icon="model.Details.LicenseSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:rules="[textRule]"
hide-details box flat
browser-autocomplete="off"
auto-grow
:label="$gettext('License')"
placeholder=""
:rows="1"
color="secondary-dark"
class="input-license"
></v-textarea>
</v-flex>
<v-flex xs12 class="pa-2">
<v-textarea
v-model="model.Details.Subject"
:append-icon="model.Details.SubjectSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:rules="[textRule]"
hide-details box flat
browser-autocomplete="off"
auto-grow
:label="$gettext('Subject')"
placeholder=""
:rows="1"
color="secondary-dark"
class="input-subject"
></v-textarea>
</v-flex>
<v-flex xs12 class="pa-2">
<v-textarea
v-model="model.Details.Subject"
:append-icon="model.Details.SubjectSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
:rules="[textRule]"
hide-details box flat
browser-autocomplete="off"
auto-grow
:label="$gettext('Subject')"
placeholder=""
:rows="1"
color="secondary-dark"
class="input-subject"
></v-textarea>
</v-flex>
<v-flex xs12 class="pa-2">
<v-textarea
v-model="model.Description"
:append-icon="model.DescriptionSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
auto-grow
:label="$gettext('Description')"
placeholder=""
:rows="1"
color="secondary-dark"
class="input-description"
></v-textarea>
</v-flex>
<v-flex xs12 class="pa-2">
<v-textarea
v-model="model.Description"
:append-icon="model.DescriptionSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
auto-grow
:label="$gettext('Description')"
placeholder=""
:rows="1"
color="secondary-dark"
class="input-description"
></v-textarea>
</v-flex>
<v-flex xs12 md8 class="pa-2">
<v-textarea
v-model="model.Details.Keywords"
:append-icon="model.Details.KeywordsSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
auto-grow
:label="$gettext('Keywords')"
placeholder=""
:rows="1"
color="secondary-dark"
class="input-keywords"
></v-textarea>
</v-flex>
<v-flex xs12 md8 class="pa-2">
<v-textarea
v-model="model.Details.Keywords"
:append-icon="model.Details.KeywordsSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
auto-grow
:label="$gettext('Keywords')"
placeholder=""
:rows="1"
color="secondary-dark"
class="input-keywords"
></v-textarea>
</v-flex>
<v-flex xs12 md4 class="pa-2">
<v-textarea
v-model="model.Details.Notes"
:append-icon="model.Details.NotesSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
auto-grow
:label="$gettext('Notes')"
placeholder=""
:rows="1"
color="secondary-dark"
class="input-notes"
></v-textarea>
</v-flex>
<v-flex xs12 md4 class="pa-2">
<v-textarea
v-model="model.Details.Notes"
:append-icon="model.Details.NotesSrc === 'manual' ? 'check' : ''"
:disabled="disabled"
hide-details box flat
browser-autocomplete="off"
auto-grow
:label="$gettext('Notes')"
placeholder=""
:rows="1"
color="secondary-dark"
class="input-notes"
></v-textarea>
</v-flex>
<v-flex v-if="!disabled" xs12 :text-xs-right="!rtl" :text-xs-left="rtl" class="pt-3">
<v-btn depressed color="secondary-light" class="compact action-close"
@click.stop="close">
<translate>Close</translate>
</v-btn>
<v-btn color="primary-button" depressed dark class="compact action-apply action-approve"
@click.stop="save(false)">
<span v-if="$config.feature('review') && model.Quality < 3"><translate>Approve</translate></span>
<span v-else><translate>Apply</translate></span>
</v-btn>
<v-btn color="primary-button" depressed dark class="compact action-done hidden-xs-only"
@click.stop="save(true)">
<translate>Done</translate>
</v-btn>
</v-flex>
</v-layout>
</v-flex>
</v-layout>
<div class="mt-1 clear"></div>
</v-form>
<v-flex v-if="!disabled" xs12 :text-xs-right="!rtl" :text-xs-left="rtl" class="pt-3">
<v-btn depressed color="secondary-light" class="compact action-close"
@click.stop="close">
<translate>Close</translate>
</v-btn>
<v-btn color="primary-button" depressed dark class="compact action-apply action-approve"
@click.stop="save(false)">
<span v-if="$config.feature('review') && model.Quality < 3"><translate>Approve</translate></span>
<span v-else><translate>Apply</translate></span>
</v-btn>
<v-btn color="primary-button" depressed dark class="compact action-done hidden-xs-only"
@click.stop="save(true)">
<translate>Done</translate>
</v-btn>
</v-flex>
</v-layout>
</v-flex>
</v-layout>
<div class="mt-1 clear"></div>
</v-form>
</v-container>
</div>
</template>
@ -471,37 +475,6 @@ export default {
this.time = taken.toFormat("HH:mm:ss");
},
pastePosition(event) {
// Auto-fills 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 model values.
this.model.Lat = lat;
this.model.Lng = lng;
// Prevent default action.
event.preventDefault();
}
}
},
updateModel() {
if (!this.model.hasId()) {
return;

View file

@ -5,7 +5,7 @@
<v-expansion-panel-content v-if="!file.Missing" :key="file.UID" class="pa-0 elevation-0 secondary-light"
style="margin-top: 1px;">
<template #header>
<div class="caption filename">{{ file.baseName(70) }}</div>
<div class="caption">{{ file.baseName(70) }}</div>
</template>
<v-card>
<v-card-text class="white pa-0">
@ -77,25 +77,25 @@
<td title="Unique ID">
UID
</td>
<td><span class="clickable" @click.stop.prevent="copyText(file.UID)">{{ file.UID | uppercase }}</span></td>
<td>{{ file.UID | uppercase }}</td>
</tr>
<tr v-if="file.InstanceID" title="XMP">
<td>
<translate>Instance ID</translate>
</td>
<td><span class="clickable" @click.stop.prevent="copyText(file.InstanceID)">{{ file.InstanceID | uppercase }}</span></td>
<td>{{ file.InstanceID | uppercase }}</td>
</tr>
<tr>
<td title="SHA-1">
<translate>Hash</translate>
</td>
<td><span class="clickable" @click.stop.prevent="copyText(file.Hash)">{{ file.Hash }}</span></td>
<td>{{ file.Hash }}</td>
</tr>
<tr v-if="file.Name">
<td>
<translate>Filename</translate>
</td>
<td><span class="clickable" @click.stop.prevent="copyText(file.Name)">{{ file.Name }}</span></td>
<td>{{ file.Name }}</td>
</tr>
<tr v-if="file.Root">
<td>
@ -107,7 +107,7 @@
<td>
<translate>Original Name</translate>
</td>
<td><span class="clickable" @click.stop.prevent="copyText(file.OriginalName)">{{ file.OriginalName }}</span></td>
<td>{{ file.OriginalName }}</td>
</tr>
<tr>
<td>
@ -207,7 +207,7 @@
hide-details
color="secondary-dark"
:items="options.Orientations()"
:readonly="readonly || !features.edit || file.Error || (file.Frames && file.Frames > 1) || (file.Duration && file.Duration > 1) || (file.FileType !== 'jpg' && file.FileType !== 'png')"
:readonly="readonly || !features.edit || (file.FileType !== 'jpg' && file.FileType !== 'png') || file.Error"
:disabled="busy"
class="input-orientation"
@change="changeOrientation(file)">
@ -294,10 +294,7 @@ export default {
type: Object,
default: () => {},
},
uid: {
type: String,
default: "",
},
uid: String,
},
data() {
return {
@ -332,18 +329,6 @@ export default {
},
computed: {},
methods: {
async copyText(text) {
if (!text) {
return;
}
try {
await Util.copyToMachineClipboard(text);
this.$notify.success(this.$gettext("Copied to clipboard"));
} catch (error) {
this.$notify.error(this.$gettext("Failed copying to clipboard"));
}
},
orientationClass(file) {
if (!file) {
return [];
@ -380,11 +365,8 @@ export default {
}
const name = m.Name;
const path = name.substring(0, name.lastIndexOf('/'));
// "#" chars in path names must be explicitly escaped,
// see https://github.com/photoprism/photoprism/issues/3695
const path = name.substring(0, name.lastIndexOf('/'))
.replaceAll(":", "%3A").replaceAll("#", "%23");
return this.$router.resolve({ path: '/index/files/' + path }).href;
},
downloadFile(file) {

View file

@ -1,24 +1,23 @@
<template>
<div class="p-tab p-tab-photo-advanced">
<v-form ref="form" lazy-validation dense accept-charset="UTF-8" @submit.prevent>
<div class="v-table__overflow">
<table class="v-datatable v-table theme--light">
<tbody>
<tr>
<td>UID</td>
<td><span class="clickable" @click.stop.prevent="copyText(model.UID)">{{ model.UID | uppercase }}</span></td>
</tr>
<tr v-if="model.DocumentID">
<td>Document ID</td>
<td><span class="clickable" @click.stop.prevent="copyText(model.DocumentID)">{{ model.DocumentID | uppercase }}</span></td>
</tr>
<tr>
<td :title="model.TypeSrc">
<translate>Type</translate>
<v-icon v-if="model.TypeSrc === 'manual'" class="src">check</v-icon>
</td>
<td>
<v-select
<div class="v-table__overflow">
<table class="v-datatable v-table theme--light">
<tbody>
<tr>
<td>UID</td>
<td>{{ model.UID | uppercase }}</td>
</tr>
<tr v-if="model.DocumentID">
<td>Document ID</td>
<td>{{ model.DocumentID | uppercase }}</td>
</tr>
<tr>
<td :title="model.TypeSrc">
<translate>Type</translate>
<v-icon v-if="model.TypeSrc === 'manual'" class="src">check</v-icon>
</td>
<td>
<v-select
v-model="model.Type"
flat solo
browser-autocomplete="off"
@ -27,27 +26,27 @@
:items="options.PhotoTypes()"
class="input-type"
@change="save">
</v-select>
</td>
</tr>
<tr v-if="model.Path">
<td>
<translate>Folder</translate>
</td>
<td><span class="clickable" @click.stop.prevent="copyText(model.Path)">{{ model.Path }}</span></td>
</tr>
<tr>
<td>
<translate>Name</translate>
</td>
<td><span class="clickable" @click.stop.prevent="copyText(model.Name)">{{ model.Name }}</span></td>
</tr>
<tr v-if="model.OriginalName">
<td>
<translate>Original Name</translate>
</td>
<td>
<v-text-field
</v-select>
</td>
</tr>
<tr v-if="model.Path">
<td>
<translate>Folder</translate>
</td>
<td>{{ model.Path }}</td>
</tr>
<tr>
<td>
<translate>Name</translate>
</td>
<td>{{ model.Name }}</td>
</tr>
<tr v-if="model.OriginalName">
<td>
<translate>Original Name</translate>
</td>
<td>
<v-text-field
v-model="model.OriginalName"
flat solo dense hide-details
browser-autocomplete="off"
@ -55,71 +54,64 @@
autocapitalize="none"
color="secondary-dark"
@change="save"
></v-text-field>
</td>
</tr>
<tr>
<td :title="sourceName(model.TitleSrc)">
<translate>Title</translate>
<v-icon v-if="model.TitleSrc === 'manual'" class="src">check</v-icon>
</td>
<td :title="sourceName(model.TitleSrc)">
<span class="clickable" @click.stop.prevent="copyText(model.Title)">{{ model.Title }}</span>
<v-icon v-if="model.TitleSrc === 'name'" class="src">insert_drive_file</v-icon>
</td>
</tr>
<tr>
<td :title="sourceName(model.TakenSrc)">
<translate>Taken</translate>
<v-icon v-if="model.TakenSrc === 'manual'" class="src">check</v-icon>
</td>
<td :title="sourceName(model.TakenSrc)">
{{ model.getDateString() }}
<v-icon v-if="model.TakenSrc === 'name' || model.TakenSrc === 'estimate'" class="src">insights</v-icon>
</td>
</tr>
<tr v-if="albums.length > 0">
<td>
<translate>Albums</translate>
</td>
<td>
<a v-for="(a, i) in albums" :key="i" :href="a.url" class="primary--text text-link"
target="_blank"><span v-if="i > 0">, </span>{{ a.title }}</a>
</td>
</tr>
<tr>
<td>
<translate>Quality Score</translate>
</td>
<td>
<v-rating v-model="model.Quality" :length="7" readonly small></v-rating>
</td>
</tr>
<tr>
<td>
<translate>Resolution</translate>
</td>
<td>{{ model.Resolution }} MP</td>
</tr>
<tr v-if="model.Faces > 0">
<td>
<translate>Faces</translate>
</td>
<td>{{ model.Faces }}</td>
</tr>
<tr v-if="model.CameraSerial">
<td>
<translate>Camera Serial</translate>
</td>
<td>{{ model.CameraSerial }}
</td>
</tr>
<tr v-if="model.Stack < 1">
<td>
<translate>Stackable</translate>
</td>
<td>
<v-switch
></v-text-field>
</td>
</tr>
<tr>
<td :title="model.TitleSrc">
<translate>Title</translate>
<v-icon v-if="model.TitleSrc === 'manual'" class="src">check</v-icon>
</td>
<td>{{ model.Title }}</td>
</tr>
<tr>
<td :title="model.TakenSrc">
<translate>Taken</translate>
<v-icon v-if="model.TakenSrc === 'manual'" class="src">check</v-icon>
</td>
<td>{{ model.getDateString() }}</td>
</tr>
<tr v-if="albums.length > 0">
<td>
<translate>Albums</translate>
</td>
<td>
<a v-for="(a, i) in albums" :key="i" :href="a.url" class="primary--text text-link" target="_blank"><span v-if="i > 0">, </span>{{ a.title }}</a>
</td>
</tr>
<tr>
<td>
<translate>Quality Score</translate>
</td>
<td>
<v-rating v-model="model.Quality" :length="7" readonly small></v-rating>
</td>
</tr>
<tr>
<td>
<translate>Resolution</translate>
</td>
<td>{{ model.Resolution }} MP</td>
</tr>
<tr v-if="model.Faces > 0">
<td>
<translate>Faces</translate>
</td>
<td>{{ model.Faces }}</td>
</tr>
<tr v-if="model.CameraSerial">
<td>
<translate>Camera Serial</translate>
</td>
<td>{{ model.CameraSerial }}
</td>
</tr>
<tr v-if="model.Stack < 1">
<td>
<translate>Stackable</translate>
</td>
<td>
<v-switch
v-model="model.Stack"
hide-details
class="input-stackable"
@ -127,105 +119,104 @@
:false-value="-1"
:label="model.Stack > - 1 ? $gettext('Yes') : $gettext('No')"
@change="save"
></v-switch>
</td>
</tr>
<tr>
<td>
<translate>Favorite</translate>
</td>
<td>
<v-switch
></v-switch>
</td>
</tr>
<tr>
<td>
<translate>Favorite</translate>
</td>
<td>
<v-switch
v-model="model.Favorite"
hide-details
class="input-favorite"
:label="model.Favorite ? $gettext('Yes') : $gettext('No')"
@change="save"
></v-switch>
</td>
</tr>
<tr v-if="$config.feature('private')">
<td>
<translate>Private</translate>
</td>
<td>
<v-switch
></v-switch>
</td>
</tr>
<tr v-if="$config.feature('private')">
<td>
<translate>Private</translate>
</td>
<td>
<v-switch
v-model="model.Private"
hide-details
class="input-private"
:label="model.Private ? $gettext('Yes') : $gettext('No')"
@change="save"
></v-switch>
</td>
</tr>
<tr>
<td>
<translate>Scan</translate>
</td>
<td>
<v-switch
></v-switch>
</td>
</tr>
<tr>
<td>
<translate>Scan</translate>
</td>
<td>
<v-switch
v-model="model.Scan"
hide-details
class="input-scan"
:label="model.Scan ? $gettext('Yes') : $gettext('No')"
@change="save"
></v-switch>
</td>
</tr>
<tr>
<td>
<translate>Panorama</translate>
</td>
<td>
<v-switch
></v-switch>
</td>
</tr>
<tr>
<td>
<translate>Panorama</translate>
</td>
<td>
<v-switch
v-model="model.Panorama"
hide-details
class="input-panorama"
:label="model.Panorama ? $gettext('Yes') : $gettext('No')"
@change="save"
></v-switch>
</td>
</tr>
<tr>
<td :title="sourceName(model.PlaceSrc)">
<translate>Place</translate>
<v-icon v-if="model.PlaceSrc === 'manual'" class="src">check</v-icon>
</td>
<td :title="sourceName(model.PlaceSrc)">
{{ model.locationInfo() }}
<v-icon v-if="model.PlaceSrc === 'estimate'" class="src">insights</v-icon>
</td>
</tr>
<tr v-if="model.Lat">
<td>
<translate>Latitude</translate>
</td>
<td>
{{ model.Lat }}
</td>
</tr>
<tr v-if="model.Lng">
<td>
<translate>Longitude</translate>
</td>
<td>
{{ model.Lng }}
</td>
</tr>
<tr v-if="model.Altitude">
<td>
<translate>Altitude</translate>
</td>
<td>
{{ model.Altitude }} m
</td>
</tr>
<tr v-if="model.Lat">
<td>
<translate>Accuracy</translate>
</td>
<td>
<v-text-field
></v-switch>
</td>
</tr>
<tr>
<td :title="model.PlaceSrc">
<translate>Place</translate>
<v-icon v-if="model.PlaceSrc === 'manual'" class="src">check</v-icon>
</td>
<td>
{{ model.locationInfo() }}
</td>
</tr>
<tr v-if="model.Lat">
<td>
<translate>Latitude</translate>
</td>
<td>
{{ model.Lat }}
</td>
</tr>
<tr v-if="model.Lng">
<td>
<translate>Longitude</translate>
</td>
<td>
{{ model.Lng }}
</td>
</tr>
<tr v-if="model.Altitude">
<td>
<translate>Altitude</translate>
</td>
<td>
{{ model.Altitude }} m
</td>
</tr>
<tr v-if="model.Lat">
<td>
<translate>Accuracy</translate>
</td>
<td>
<v-text-field
v-model="model.CellAccuracy"
flat solo dense hide-details
browser-autocomplete="off"
@ -236,53 +227,52 @@
suffix="m"
style="width: 100px;"
@change="save"
></v-text-field>
</td>
</tr>
<tr>
<td>
<translate>Created</translate>
</td>
<td>
{{ formatTime(model.CreatedAt) }}
</td>
</tr>
<tr>
<td>
<translate>Updated</translate>
</td>
<td>
{{ formatTime(model.UpdatedAt) }}
</td>
</tr>
<tr v-if="model.EditedAt">
<td>
<translate>Edited</translate>
</td>
<td>
{{ formatTime(model.EditedAt) }}
</td>
</tr>
<tr v-if="model.CheckedAt">
<td>
<translate>Checked</translate>
</td>
<td>
{{ formatTime(model.CheckedAt) }}
</td>
</tr>
<tr v-if="model.DeletedAt">
<td>
<translate>Archived</translate>
</td>
<td>
{{ formatTime(model.DeletedAt) }}
</td>
</tr>
</tbody>
</table>
</div>
</v-form>
></v-text-field>
</td>
</tr>
<tr>
<td>
<translate>Created</translate>
</td>
<td>
{{ formatTime(model.CreatedAt) }}
</td>
</tr>
<tr>
<td>
<translate>Updated</translate>
</td>
<td>
{{ formatTime(model.UpdatedAt) }}
</td>
</tr>
<tr v-if="model.EditedAt">
<td>
<translate>Edited</translate>
</td>
<td>
{{ formatTime(model.EditedAt) }}
</td>
</tr>
<tr v-if="model.CheckedAt">
<td>
<translate>Checked</translate>
</td>
<td>
{{ formatTime(model.CheckedAt) }}
</td>
</tr>
<tr v-if="model.DeletedAt">
<td>
<translate>Archived</translate>
</td>
<td>
{{ formatTime(model.DeletedAt) }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
@ -290,21 +280,15 @@
import Thumb from "model/thumb";
import {DateTime, Info} from "luxon";
import * as options from "options/options";
import {T} from "common/vm";
import Util from "common/util";
export default {
name: 'PTabPhotoAdvanced',
props: {
model: {
type: Object,
default: () => {
},
},
uid: {
type: String,
default: "",
default: () => {},
},
uid: String,
},
data() {
return {
@ -340,40 +324,6 @@ export default {
},
},
methods: {
async copyText(text) {
if (!text) {
return;
}
try {
await Util.copyToMachineClipboard(text);
this.$notify.success(this.$gettext("Copied to clipboard"));
} catch (error) {
this.$notify.error(this.$gettext("Failed copying to clipboard"));
}
},
sourceName(s) {
switch (s) {
case "":
case "auto":
case "manual":
return "";
case "meta":
return this.$gettext('Metadata');
case "xmp":
return "XMP";
case "estimate":
return this.$gettext("Estimate");
case "name":
return this.$gettext("Name");
case "image":
return this.$gettext("Image");
case "location":
return this.$gettext("Location");
default:
return T(Util.capitalize(s));
}
},
formatTime(s) {
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_MED);
},
@ -391,7 +341,7 @@ export default {
return '#';
}
return this.$router.resolve({name: 'album', params: {uid: m.UID, slug: 'view'}}).href;
return this.$router.resolve({ name: 'album', params: { uid: m.UID, slug: 'view' }}).href;
},
},
};

View file

@ -1,117 +1,102 @@
<template>
<div class="p-tab p-tab-photo-labels">
<v-form ref="form" lazy-validation dense accept-charset="UTF-8" @submit.prevent>
<v-layout class="pa-2-md-and-up" row wrap align-top fill-height>
<v-flex class="pa-2 hidden-sm-and-down" xs12 md2 xxl1 fill-height>
<p-photo-preview :model="model"></p-photo-preview>
</v-flex>
<v-flex class="pa-2-md-and-up ra-4-table-md-and-up" xs12 md10 xxl11 fill-width fill-height>
<v-data-table
v-model="selected"
:headers="listColumns"
:items="model.Labels"
hide-actions
class="elevation-0 p-results"
disable-initial-sort
item-key="ID"
:no-data-text="$gettext('No labels found')"
<v-data-table
v-model="selected"
:headers="listColumns"
:items="model.Labels"
hide-actions
class="elevation-0 p-files p-files-list p-results"
disable-initial-sort
item-key="ID"
:no-data-text="$gettext('No labels found')"
>
<template #items="props" class="p-file">
<td>
<v-edit-dialog
:return-value.sync="props.item.Label.Name"
lazy
class="p-inline-edit"
@save="renameLabel(props.item.Label)"
>
<template #items="props" class="p-file">
<td>
<v-edit-dialog
:return-value.sync="props.item.Label.Name"
lazy
class="p-inline-edit"
@save="renameLabel(props.item.Label)"
>
{{ props.item.Label.Name }}
<template #input>
<v-text-field
v-model="props.item.Label.Name"
:rules="[nameRule]"
:label="$gettext('Name')"
color="secondary-dark"
class="input-rename background-inherit elevation-0"
single-line autofocus solo hide-details
></v-text-field>
</template>
</v-edit-dialog>
</td>
<td class="text-xs-left">{{ sourceName(props.item.LabelSrc) }}</td>
<td class="text-xs-center">{{ 100 - props.item.Uncertainty }}%</td>
<td class="text-xs-center">
<v-btn v-if="disabled" icon small flat :ripple="false"
class="action-view" title="Search"
@click.stop.prevent="searchLabel(props.item.Label)">
<v-icon color="secondary-dark">search</v-icon>
</v-btn>
<v-btn v-else-if="props.item.Uncertainty < 100 && props.item.LabelSrc === 'manual'" icon
small flat :ripple="false"
class="action-delete" title="Delete"
@click.stop.prevent="removeLabel(props.item.Label)">
<v-icon color="secondary-dark">delete</v-icon>
</v-btn>
<v-btn v-else-if="props.item.Uncertainty < 100" icon small flat :ripple="false"
class="action-remove" title="Remove"
@click.stop.prevent="removeLabel(props.item.Label)">
<v-icon color="secondary-dark">remove</v-icon>
</v-btn>
<v-btn v-else icon small flat :ripple="false"
class="action-on" title="Activate"
@click.stop.prevent="activateLabel(props.item.Label)">
<v-icon color="secondary-dark">add</v-icon>
</v-btn>
</td>
</template>
<template v-if="!disabled" #footer>
<td>
<v-text-field
v-model="newLabel"
{{ props.item.Label.Name }}
<template #input>
<v-text-field
v-model="props.item.Label.Name"
:rules="[nameRule]"
color="secondary-dark"
browser-autocomplete="off"
:label="$gettext('Name')"
single-line
flat solo hide-details
autofocus
class="input-label"
@keyup.enter.native="addLabel"
></v-text-field>
</td>
<td class="text-xs-left">{{ sourceName('manual') }}</td>
<td class="text-xs-center">100%</td>
<td class="text-xs-center">
<v-btn icon small flat :ripple="false" title="Add"
class="p-photo-label-add"
@click.stop.prevent="addLabel">
<v-icon color="secondary-dark">add</v-icon>
</v-btn>
</td>
color="secondary-dark"
class="input-rename background-inherit elevation-0"
single-line autofocus solo hide-details
></v-text-field>
</template>
</v-data-table>
</v-flex>
</v-layout>
<!-- div class="mt-1 clear"></div -->
</v-form>
</v-edit-dialog>
</td>
<td class="text-xs-left">{{ sourceName(props.item.LabelSrc) }}</td>
<td class="text-xs-center">{{ 100 - props.item.Uncertainty }}%</td>
<td class="text-xs-center">
<v-btn v-if="disabled" icon small flat :ripple="false"
class="action-view" title="Search"
@click.stop.prevent="searchLabel(props.item.Label)">
<v-icon color="secondary-dark">search</v-icon>
</v-btn>
<v-btn v-else-if="props.item.Uncertainty < 100 && props.item.LabelSrc === 'manual'" icon
small flat :ripple="false"
class="action-delete" title="Delete"
@click.stop.prevent="removeLabel(props.item.Label)">
<v-icon color="secondary-dark">delete</v-icon>
</v-btn>
<v-btn v-else-if="props.item.Uncertainty < 100" icon small flat :ripple="false"
class="action-remove" title="Remove"
@click.stop.prevent="removeLabel(props.item.Label)">
<v-icon color="secondary-dark">remove</v-icon>
</v-btn>
<v-btn v-else icon small flat :ripple="false"
class="action-on" title="Activate"
@click.stop.prevent="activateLabel(props.item.Label)">
<v-icon color="secondary-dark">add</v-icon>
</v-btn>
</td>
</template>
<template v-if="!disabled" #footer>
<td>
<v-text-field
v-model="newLabel"
:rules="[nameRule]"
color="secondary-dark"
browser-autocomplete="off"
:label="$gettext('Name')"
single-line
flat solo hide-details
autofocus
class="input-label"
@keyup.enter.native="addLabel"
></v-text-field>
</td>
<td class="text-xs-left">{{ sourceName('manual') }}</td>
<td class="text-xs-center">100%</td>
<td class="text-xs-center">
<v-btn icon small flat :ripple="false" title="Add"
class="p-photo-label-add"
@click.stop.prevent="addLabel">
<v-icon color="secondary-dark">add</v-icon>
</v-btn>
</td>
</template>
</v-data-table>
</div>
</template>
<script>
import Label from "model/label";
import Thumb from "model/thumb";
export default {
name: 'PTabPhotoLabels',
props: {
model: {
type: Object,
default: () => {
},
},
uid: {
type: String,
default: "",
default: () => {},
},
uid: String,
},
data() {
return {
@ -185,9 +170,6 @@ export default {
this.$router.push({name: 'all', query: {q: 'label:' + label.Slug}}).catch(() => {});
this.$emit('close');
},
openPhoto() {
this.$viewer.show(Thumb.fromFiles([this.model]), 0);
},
},
};
</script>

View file

@ -13,7 +13,7 @@
<translate>Recognition starts after indexing has been completed.</translate>
</p>
</v-alert>
<v-layout class="search-results face-results cards-view" row wrap fill-height>
<v-layout row wrap class="search-results face-results cards-view">
<v-flex
v-for="(marker, index) in markers"
:key="index"

View file

@ -408,7 +408,7 @@ export default {
for (let i = 0; i < thumbs.length; i++) {
let t = thumbs[i];
result.push({"text": t.w + ' × ' + t.h, "value": t.size});
result.push({"text": t.w + 'x' + t.h, "value": t.size});
}
return result;

View file

@ -62,16 +62,12 @@
<p class="px-2 ma-0 text-xs-right opacity-85"><span v-if="eta">{{ eta }}</span></p>
</v-progress-linear>
<p v-if="isDemo" class="body-2">
<translate :translate-params="{n: fileLimit}">You can upload up to %{n} files for test purposes.</translate>
<translate>Please do not upload any private, unlawful or offensive pictures. </translate>
</p>
<p v-else-if="rejectNSFW" class="body-2">
<p v-if="safe" class="body-1">
<translate>Please don't upload photos containing offensive content.</translate>
<translate>Uploads that may contain such images will be rejected automatically.</translate>
</p>
<p v-if="featReview" class="body-1">
<p v-if="review" class="body-1">
<translate>Non-photographic and low-quality images require a review before they appear in search results.</translate>
</p>
@ -103,13 +99,8 @@ export default {
name: 'PUploadDialog',
props: {
show: Boolean,
data: {
type: Object,
default: () => {},
},
},
data() {
const isDemo = this.$config.get("demo");
return {
albums: [],
selectedAlbums: [],
@ -129,29 +120,16 @@ export default {
remainingTime: -1,
eta: "",
token: "",
isDemo: isDemo,
fileLimit: isDemo ? 3 : 0,
rejectNSFW: !this.$config.get("uploadNSFW"),
featReview: this.$config.feature("review"),
review: this.$config.feature("review"),
safe: !this.$config.get("uploadNSFW"),
rtl: this.$rtl,
};
},
watch: {
show: function () {
this.reset();
this.isDemo = this.$config.get("demo");
this.fileLimit = this.isDemo ? 3 : 0;
this.rejectNSFW = !this.$config.get("uploadNSFW");
this.featReview = this.$config.feature("review");
// Set currently selected albums.
if (this.data && Array.isArray(this.data.albums)) {
this.selectedAlbums = this.data.albums;
} else {
this.selectedAlbums = [];
}
// Fetch albums from backend.
this.review = this.$config.feature("review");
this.safe = !this.$config.get("uploadNSFW");
this.findAlbums("");
}
},
@ -202,8 +180,8 @@ export default {
this.failed = false;
this.current = 0;
this.total = 0;
this.totalSize = 0;
this.totalFailed = 0;
this.totalSize = 0;
this.completedSize = 0;
this.completedTotal = 0;
this.started = 0;
@ -255,19 +233,10 @@ export default {
return;
}
const files = this.$refs.upload.files;
this.selected = this.$refs.upload.files;
this.total = this.selected.length;
// Too many files selected for upload?
if (this.isDemo && files && files.length > this.fileLimit) {
Notify.error(this.$gettext("Too many files selected"));
return;
}
this.selected = files;
this.total = files.length;
// No files selected?
if (!this.selected || this.total < 1) {
if (this.total < 1) {
return;
}

View file

@ -1,7 +1,6 @@
<template>
<div v-if="show" class="video-viewer" role="dialog" @click.stop.prevent="onClose" @keydown.esc.stop.prevent="onClose">
<p-video-player v-show="show" ref="player" :source="source" :poster="poster" :height="height"
:width="width" :autoplay="true" :loop="loop" @close="onClose"></p-video-player>
<p-video-player v-show="show" ref="player" :videos="videos" :index="index" :album="album" @close="onClose"></p-video-player>
</div>
</template>
<script>
@ -18,7 +17,8 @@ export default {
defaultHeight: 480,
width: 640,
height: 480,
video: null,
index: 0,
videos: [],
album: null,
loop: false,
subscriptions: [],
@ -39,9 +39,16 @@ export default {
methods: {
onOpen(ev, params) {
const fullscreen = !!params.fullscreen;
const hasQueue = params.videos && params.videos.length > 0;
this.video = params.video;
this.album = params.album;
this.videos = hasQueue ? params.videos : [];
this.album = params.album ? params.album : null;
if(params.index && params.index < this.videos.length) {
this.index = params.index;
} else {
this.index = 0;
}
this.play(fullscreen);
},
@ -58,25 +65,11 @@ export default {
this.show = false;
},
play(fullscreen) {
if (!this.video) {
this.$notify.error(this.$gettext("No video selected"));
if (!this.videos) {
this.$notify.error(this.$gettext("No videos found to play"));
return;
}
const params = this.video.videoParams();
if (params.error) {
this.$notify.error(params.error);
return;
}
// Set video parameters.
this.loop = params.loop;
this.width = params.width;
this.height = params.height;
this.poster = params.poster;
this.source = params.uri;
// Play video.
this.show = true;

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

Some files were not shown because too many files have changed in this diff Show more